作者
VictorGomes译者
王强策划
蔡芳芳参数适配器机制不仅复杂,而且成本很高。
本文最初发表于v8.dev(FasterJavaScriptcalls),基于CC3.0协议分享,由InfoQ翻译并发布。
JavaScript允许使用与预期形式参数数量不同的实际参数来调用一个函数,也就是传递的实参可以少于或者多于声明的形参数量。前者称为申请不足(under-application),后者称为申请过度(over-application)。
在申请不足的情况下,剩余形式参数会被分配undefined值。在申请过度的情况下,可以使用rest参数和arguments属性访问剩余实参,或者如果它们是多余的可以直接忽略。如今,许多Web/Node.js框架都使用这个JS特性来接受可选形参,并创建更灵活的API。
直到最近,V8都有一种专门的机制来处理参数大小不匹配的情况:这种机制叫做参数适配器框架。不幸的是,参数适配是有性能成本的,但在现代的前端和中间件框架中这种成本往往是必须的。但事实证明,我们可以通过一个巧妙的技巧来拿掉这个多余的框架,简化V8代码库并消除几乎所有的开销。
我们可以通过一个微型基准测试来计算移除参数适配器框架可以获得的性能收益。
console.time();functionf(x,y,z){}for(leti=0;iN;i++){f(1,2,3,4,5);}console.timeEnd();
移除参数适配器框架的性能收益,通过一个微基准测试来得出。
上图显示,在无JIT模式(Ignition)下运行时,开销消失,并且性能提高了11.2%。使用TurboFan时,我们的速度提高了40%。
这个微基准测试自然是为了最大程度地展现参数适配器框架的影响而设计的。但是,我们也在许多基准测试中看到了显著的改进,例如我们内部的JSTests/Array基准测试(7%)和Octane2(Richards子项为4.6%,EarleyBoyer为6.1%)。
太长不看版:反转参数
这个项目的重点是移除参数适配器框架,这个框架在访问栈中被调用者的参数时为其提供了一个一致的接口。为此,我们需要反转栈中的参数,并在被调用者框架中添加一个包含实际参数计数的新插槽。下图显示了更改前后的典型框架示例。
移除参数适配器框架之前和之后的典型JavaScript栈框架。
加快JavaScript调用
为了讲清楚我们如何加快调用,首先我们来看看V8如何执行一个调用,以及参数适配器框架如何工作。
当我们在JS中调用一个函数调用时,V8内部会发生什么呢?用以下JS脚本为例:
functionadd42(x){returnx+42;}add42(3);
在函数调用期间V8内部的执行流程。
IgnitionV8是一个多层VM。它的第一层称为Ignition,是一个具有累加器寄存器的字节码栈机。V8首先会将代码编译为Ignition字节码。上面的调用被编译为以下内容:
0dLdaUndefined;;Loadundefinedintotheaccumulator26f9Starr2;;StoreitinregisterrLdaGlobal[1];;Loadglobalpointedbyconst1(add42)26faStarr1;;Storeitinregisterr10c03LdaSmi[3];;Loadsmallinteger3intotheaccumulator26f8Starr3;;Storeitinregisterr35ffafCallNoFeedbackr1,r2-r3;;Invokecall
调用的第一个参数通常称为接收器(receiver)。接收器是JSFunction中的this对象,并且每个JS函数调用都必须有一个this。CallNoFeedback的字节码处理器需要使用寄存器列表r2-r3中的参数来调用对象r1。
在深入研究字节码处理器之前,请先注意寄存器在字节码中的编码方式。它们是负的单字节整数:r1编码为fa,r2编码为f9,r3编码为f8。我们可以将任何寄存器ri称为fb-i,实际上正如我们所见,正确的编码是-2-kFixedFrameHeaderSize-i。寄存器列表使用第一个寄存器和列表的大小来编码,因此r2-r3为f。
Ignition中有许多字节码调用处理器。可以在此处查看它们的列表: