抛弃V8参数适配器框架:JavaScript调用提速40%的实践

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"参数适配器机制不仅复杂,而且成本很高。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"},{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"本文最初发表于v8.dev("},{"type":"link","attrs":{"href":"https:\/\/v8.dev\/blog\/adaptor-frame","title":null,"type":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"Faster JavaScript calls"}],"marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}]},{"type":"text","marks":[{"type":"italic"},{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"),基于CC 3.0协议分享,由InfoQ翻译并发布。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"JavaScript允许使用与预期形式参数数量不同的实际参数来调用一个函数,也就是传递的实参可以少于或者多于声明的形参数量。前者称为申请不足(under-application),后者称为申请过度(over-application)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"在申请不足的情况下,剩余形式参数会被分配undefined值。在申请过度的情况下,可以使用rest参数和arguments属性访问剩余实参,或者如果它们是多余的可以直接忽略。如今,许多Web\/Node.js框架都使用这个JS特性来接受可选形参,并创建更灵活的API。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"直到最近,V8都有一种专门的机制来处理参数大小不匹配的情况:这种机制叫做参数适配器框架。不幸的是,参数适配是有性能成本的,但在现代的前端和中间件框架中这种成本往往是必须的。但事实证明,我们可以通过一个巧妙的技巧来拿掉这个多余的框架,简化V8代码库并消除几乎所有的开销。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"我们可以通过一个微型基准测试来计算移除参数适配器框架可以获得的性能收益。"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"console.time();\nfunction f(x, y, z) {}\nfor (let i = 0; i < N; i++) {\n f(1, 2, 3, 4, 5);\n}\nconsole.timeEnd();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/da\/da93e1281867a5ca567918d1ffcf2094.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"移除参数适配器框架的性能收益,通过一个微基准测试来得出。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"上图显示,在无JIT模式(Ignition)下运行时,开销消失,并且性能提高了11.2%。使用TurboFan时,我们的速度提高了40%。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"这个微基准测试自然是为了最大程度地展现参数适配器框架的影响而设计的。但是,我们也在许多基准测试中看到了显著的改进,例如我们内部的JSTests\/Array基准测试(7%)和Octane2(Richards子项为4.6%,EarleyBoyer为6.1%)。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"太长不看版:反转参数"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"这个项目的重点是移除参数适配器框架,这个框架在访问栈中被调用者的参数时为其提供了一个一致的接口。为此,我们需要反转栈中的参数,并在被调用者框架中添加一个包含实际参数计数的新插槽。下图显示了更改前后的典型框架示例。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/bc\/bc5a7fc5ae81d40659f452ca8f44867a.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" 移除参数适配器框架之前和之后的典型JavaScript栈框架。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"加快JavaScript调用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"为了讲清楚我们如何加快调用,首先我们来看看V8如何执行一个调用,以及参数适配器框架如何工作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"当我们在JS中调用一个函数调用时,V8内部会发生什么呢?用以下JS脚本为例:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function add42(x) {\n return x + 42;\n}\nadd42(3);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/52\/52e0ad1d61c3ae3d84388a315bf320ba.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"在函数调用期间V8内部的执行流程。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Ignition"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"V8是一个多层VM。它的第一层称为Ignition,是一个具有累加器寄存器的字节码栈机。V8首先会将代码编译为Ignition字节码。上面的调用被编译为以下内容:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"0d LdaUndefined ;; Load undefined into the accumulator\n26 f9 Star r2 ;; Store it in register r2\n13 01 00 LdaGlobal [1] ;; Load global pointed by const 1 (add42)\n26 fa Star r1 ;; Store it in register r1\n0c 03 LdaSmi [3] ;; Load small integer 3 into the accumulator\n26 f8 Star r3 ;; Store it in register r3\n5f fa f9 02 CallNoFeedback r1, r2-r3 ;; Invoke call"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"调用的第一个参数通常称为接收器(receiver)。接收器是JSFunction中的this对象,并且每个JS函数调用都必须有一个this。CallNoFeedback的字节码处理器需要使用寄存器列表r2-r3中的参数来调用对象r1。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"在深入研究字节码处理器之前,请先注意寄存器在字节码中的编码方式。它们是负的单字节整数:r1编码为fa,r2编码为f9,r3编码为f8。我们可以将任何寄存器ri称为fb - i,实际上正如我们所见,正确的编码是- 2 - kFixedFrameHeaderSize - i。寄存器列表使用第一个寄存器和列表的大小来编码,因此r2-r3为f9 02。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"Ignition中有许多字节码调用处理器。可以在此处查看它们的"},{"type":"link","attrs":{"href":"https:\/\/source.chromium.org\/chromium\/chromium\/src\/+\/master:v8\/src\/interpreter\/bytecodes.h;drc=3965dcd5cb1141c90f32706ac7c965dc5c1c55b3;l=184","title":"xxx","type":null},"content":[{"type":"text","text":"列表"}]},{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"。它们彼此之间略有不同。有些字节码针对undefined的接收器调用、属性调用、具有固定数量的参数调用或通用调用进行了优化。在这里我们分析CallNoFeedback,这是一个通用调用,在该调用中我们不会积累执行过程中的反馈。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"这个字节码的处理器非常简单。它是用CodeStubAssembler编写的,你可以"},{"type":"link","attrs":{"href":"https:\/\/source.chromium.org\/chromium\/chromium\/src\/+\/master:v8\/src\/interpreter\/interpreter-generator.cc;drc=6cdb24a4ce9d4151035c1f133833137d2e2881d1;l=1467","title":"xxx","type":null},"content":[{"type":"text","text":"在此处"}]},{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"查看。本质上,它会尾调用一个架构依赖的内置InterpreterPushArgsThenCall。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"这个内置方法实际上是将返回地址弹出到一个临时寄存器中,压入所有参数(包括接收器),然后压回该返回地址。此时,我们不知道被调用者是否是可调用对象,也不知道被调用者期望多少个参数,也就是它的形式参数数量。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/38\/3842b103ec03a412518a2e91196ae2e6.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" 内置InterpreterPushArgsThenCall执行后的框架状态。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"最终,执行会尾调用到内置的Call。它会在那里检查目标是否是适当的函数、构造器或任何可调用对象。它还会读取共享shared function info结构以获得其形式参数计数。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"如果被调用者是一个函数对象,它将对内置的CallFunction进行尾部调用,并在其中进行一系列检查,包括是否有undefined对象作为接收器。如果我们有一个undefined或null对象作为接收器,则应根据ECMA规范对其修补,以引用全局代理对象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"执行随后会对内置的InvokeFunctionCode进行尾调用。在没有参数不匹配的情况下,InvokeFunctionCode只会调用被调用对象中字段Code所指向的内容。这可以是一个优化函数,也可以是内置的InterpreterEntryTrampoline。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"如果我们假设要调用的函数尚未优化,则Ignition trampoline将设置一个IntepreterFrame。你可以"},{"type":"link","attrs":{"href":"https:\/\/source.chromium.org\/chromium\/chromium\/src\/+\/master:v8\/src\/execution\/frame-constants.h;drc=574ac5d62686c3de8d782dc798337ce1355dc066;l=14","title":"xxx","type":null},"content":[{"type":"text","text":"在此处查看"}]},{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"V8中框架类型的简短摘要。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"接下来发生的事情就不用多谈了,我们可以看一个被调用者执行期间的解释器框架快照。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/b0\/b0f0077a23a4bf413cf442bfa1bf44f4.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"我们看到框架中有固定数量的插槽:返回地址、前一个框架指针、上下文、我们正在执行的当前函数对象、该函数的字节码数组以及我们当前正在执行的字节码偏移量。最后,我们有一个专用于此函数的寄存器列表(你可以将它们视为函数局部变量)。add42函数实际上没有任何寄存器,但是调用者具有类似的框架,其中包含3个寄存器。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"如预期的那样,add42是一个简单的函数:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"25 02 Ldar a0 ;; Load the first argument to the accumulator\n40 2a 00 AddSmi [42] ;; Add 42 to it\nab Return ;; Return the accumulator"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"请注意我们在Ldar(Load Accumulator Register)字节码中编码参数的方式:参数1(a0)用数字02编码。实际上,任何参数的编码规则都是[ai] = 2 + parameter_count - i - 1,接收器[this] = 2 + parameter_count,或者在本例中[this] = 3。此处的参数计数不包括接收器。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"现在我们就能理解为什么用这种方式对寄存器和参数进行编码。它们只是表示一个框架指针的偏移量。然后,我们可以用相同的方式处理参数\/寄存器的加载和存储。框架指针的最后一个参数偏移量为2(先前的框架指针和返回地址)。这就解释了编码中的2。解释器框架的固定部分是6个插槽(4个来自框架指针),因此寄存器零位于偏移量-5处,也就是fb,寄存器1位于fa处。很聪明是吧?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"但请注意,为了能够访问参数,该函数必须知道栈中有多少个参数!无论有多少参数,索引2都指向最后一个参数!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"Return的字节码处理器将调用内置的LeaveInterpreterFrame来完成。该内置函数本质上是从框架中读取函数对象以获取参数计数,弹出当前框架,恢复框架指针,将返回地址保存在一个暂存器中,根据参数计数弹出参数并跳转到暂存器中的地址。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"这套流程很棒!但是,当我们调用一个实参数量少于或多于其形参数量的函数时,会发生什么呢?这个聪明的参数\/寄存器访问流程将失败,我们该如何在调用结束时清理参数?"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"参数适配器框架"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"现在,我们使用更少或更多的实参来调用add42:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"add42();\nadd42(1, 2, 3);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"JS开发人员会知道,在第一种情况下,x将被分配undefined,并且该函数将返回undefined + 42 = NaN。在第二种情况下,x将被分配1,函数将返回43,其余参数将被忽略。请注意,调用者不知道是否会发生这种情况。即使调用者检查了参数计数,被调用者也可以使用rest参数或arguments对象访问其他所有参数。实际上,在sloppy模式下甚至可以在add42外部访问arguments对象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"如果我们执行与之前相同的步骤,则将首先调用内置的InterpreterPushArgsThenCall。它将像这样将参数推入栈:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/d3\/d38b47c6d1fbbd5b7442f8432aa0c6ad.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" 内置InterpreterPushArgsThenCall执行后的框架状态。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"继续与以前相同的过程,我们检查被调用者是否为函数对象,获取其参数计数,并将接收器补到全局代理。最终,我们到达了InvokeFunctionCode。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"在这里我们不会跳转到被调用者对象中的Code。我们检查参数大小和参数计数之间是否存在不匹配,然后跳转到ArgumentsAdaptorTrampoline。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"在这个内置组件中,我们构建了一个额外的框架,也就是臭名昭著的参数适配器框架。这里我不会解释内置组件内部发生了什么,只会向你展示内置组件调用被调用者的Code之前的框架状态。请注意,这是一个正确的x64 call(不是jmp),在被调用者执行之后,我们将返回到ArgumentsAdaptorTrampoline。这与进行尾调用的InvokeFunctionCode正好相反。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/68\/682f0b8d0c8cdf9276cbb20cede9476e.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"我们创建了另一个框架,该框架复制了所有必需的参数,以便在被调用者框架顶部精确地包含参数的形参计数。它创建了一个被调用者函数的接口,因此后者无需知道参数数量。被调用者将始终能够使用与以前相同的计算结果来访问其参数,即[ai] = 2 + parameter_count - i - 1。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"V8具有一些特殊的内置函数,它们在需要通过rest参数或arguments对象访问其余参数时能够理解适配器框架。它们始终需要检查被调用者框架顶部的适配器框架类型,然后采取相应措施。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"如你所见,我们解决了参数\/寄存器访问问题,但是却添加了很多复杂性。需要访问所有参数的内置组件都需要了解并检查适配器框架的存在。不仅如此,我们还需要注意不要访问过时的旧数据。考虑对add42的以下更改:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function add42(x) {\n x += 42;\n return x;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"现在,字节码数组为:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"25 02 Ldar a0 ;; Load the first argument to the accumulator\n40 2a 00 AddSmi [42] ;; Add 42 to it\n26 02 Star a0 ;; Store accumulator in the first argument slot\nab Return ;; Return the accumulator"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"如你所见,我们现在修改a0。因此,在调用add42(1, 2, 3)的情况下,参数适配器框架中的插槽将被修改,但调用者框架仍将包含数字1。我们需要注意,参数对象正在访问修改后的值,而不是旧值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"从函数返回很简单,只是会很慢。还记得LeaveInterpreterFrame做什么吗?它基本上会弹出被调用者框架和参数,直到到达最大形参计数为止。因此,当我们返回参数适配器存根时,栈如下所示:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/f1\/f182bba3183c5cc33de85e99baf5e07f.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"被调用者add42执行之后的框架状态。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"我们需要弹出参数数量,弹出适配器框架,根据实际参数计数弹出所有参数,然后返回到调用者执行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"简单总结:"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}},{"type":"strong"}],"text":"参数适配器机制不仅复杂,而且成本很高。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"移除参数适配器框架"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"我们可以做得更好吗?我们可以移除适配器框架吗?事实证明我们确实可以。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"我们回顾一下之前的需求:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"我们需要能够像以前一样无缝访问参数和寄存器。访问它们时无法进行检查。那成本太高了。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"我们需要能够从栈中构造rest参数和arguments对象。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"从一个调用返回时,我们需要能够轻松清理未知数量的参数。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"此外,当然我们希望没有额外的框架!"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"如果要消除多余的框架,则需要确定将参数放在何处:在被调用者框架中还是在调用者框架中。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"被调用者框架中的参数"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"假设我们将参数放在被调用者框架中。这似乎是一个好主意,因为无论何时弹出框架,我们都会一次弹出所有参数!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"参数必须位于保存的框架指针和框架末尾之间的某个位置。这就要求框架的大小不会被静态地知晓。访问参数仍然很容易,它就是一个来自框架指针的简单偏移量。但现在访问寄存器要复杂得多,因为它会根据参数的数量而变化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"栈指针总是指向最后一个寄存器,然后我们可以使用它来访问寄存器而无需知道参数计数。这种方法可能行得通,但它有一个关键缺陷。它需要复制所有可以访问寄存器和参数的字节码。我们将需要LdaArgument和LdaRegister,而不是简单的Ldar。当然,我们还可以检查我们是否正在访问一个参数或寄存器(正或负偏移量),但这将需要检查每个参数和寄存器访问。显然这种方法太昂贵了!"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"调用者框架中的参数"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"好的,如果我们在调用者框架中放参数呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"记住如何计算一个框架中参数i的偏移量:[ai] = 2 + parameter_count - i - 1。如果我们拥有所有参数(不仅是形式参数),则偏移量将为[ai] = 2 + parameter_count - i - 1.也就是说,对于每个参数访问,我们都需要加载实际的参数计数。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"但如果我们反转参数会发生什么呢?现在可以简单地将偏移量计算为[ai] = 2 + i。我们不需要知道栈中有多少个参数,但如果我们可以保证栈中至少有形参计数那么多的参数,那么我们就能一直使用这种方案来计算偏移量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"换句话说,压入栈的参数数量将始终是参数数量和形参数量之间的最大值,并且在需要时使用undefined对象进行填充。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"这还有另一个好处!对于任何JS函数,接收器始终位于相同的偏移量处,就在返回地址的正上方:[this] = 2。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"对于我们的第1和第4条要求,这是一个干净的解决方案。另外两个要求又如何呢?我们如何构造rest参数和arguments对象?返回调用者时如何清理栈中的参数?为此,我们缺少的只是参数计数而已。我们需要将其保存在某个地方。只要可以轻松访问此信息即可,具体怎么做没那么多限制。两种基本选项分别是:将其推送到调用者框架中的接收者之后,或被调用者框架中的固定标头部分。我们实现了后者,因为它合并了Interpreter和Optimized框架的固定标头部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"如果在V8 v8.9中运行前面的示例,则在InterpreterArgsThenPush之后将看到以下栈(请注意,现在参数已反转):"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/87\/87b1d2ca42d67ff2bb85af7b8af953d6.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"内置InterpreterPushArgsThenCall执行后的框架状态。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"所有执行都遵循类似的路径,直到到达InvokeFunctionCode。在这里,我们在申请不足的情况下处理参数,根据需要推送尽可能多的undefined对象。请注意,在申请过度的情况下,我们不会进行任何更改。最后,我们通过一个寄存器将参数数量传递给被调用者的Code。在x64的情况下,我们使用寄存器rax。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"如果被调用者尚未进行优化,我们将到达InterpreterEntryTrampoline,它会构建以下栈框架。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/76\/76e8d2fe335a276d9442e1d136d37289.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"没有参数适配器的栈框架。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"被调用者框架有一个额外的插槽,其中包含的参数计数可用于构造rest参数或arguments对象,并在返回到调用者之前清除栈中参数。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"返回时,我们修改LeaveInterpreterFrame以读取栈中的参数计数,并弹出参数计数和形式参数计数之间的较大数字。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"TurboFan"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"那么代码优化呢?我们来稍微更改一下初始脚本,以强制V8使用TurboFan对其进行编译:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function add42(x) { return x + 42; }\nfunction callAdd42() { add42(3); }\n%PrepareFunctionForOptimization(callAdd42);\ncallAdd42();\n%OptimizeFunctionOnNextCall(callAdd42);\ncallAdd42();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"在这里,我们使用V8内部函数来强制V8优化调用,否则V8仅在我们的小函数变热(经常使用)时才对其进行优化。我们在优化之前调用它一次,以收集一些可用于指导编译的类型信息。在此处阅读有关TurboFan的更多信息("},{"type":"link","attrs":{"href":"https:\/\/v8.dev\/docs\/turbofan","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/v8.dev\/docs\/turbofan"}],"marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}]},{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":")。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"这里,我只展示与主题相关的部分生成代码。"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"movq rdi,0x1a8e082126ad ;; Load the function object \npush 0x6 ;; Push SMI 3 as argument\nmovq rcx,0x1a8e082030d1 ;; \npush rcx ;; Push receiver (the global proxy object)\nmovl rax,0x1 ;; Save the arguments count in rax\nmovl rcx,[rdi+0x17] ;; Load function object {Code} field in rcx\ncall rcx ;; Finally, call the code object!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"尽管这段代码使用了汇编来编写,但如果你仔细看我的注释应该很容易能懂。本质上,在编译调用时,TF需要完成之前在InterpreterPushArgsThenCall、Call、CallFunction和InvokeFunctionCall内置组件中完成的所有工作。它应该会有更多的静态信息来执行此操作并发出更少的计算机指令。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"带参数适配器框架的TurboFan"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"现在,让我们来看看参数数量和参数计数不匹配的情况。考虑调用add42(1, 2, 3)。它会编译为:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"movq rdi,0x4250820fff1 ;; Load the function object \n;; Push receiver and arguments SMIs 1, 2 and 3\nmovq rcx,0x42508080dd5 ;; \npush rcx\npush 0x2\npush 0x4\npush 0x6\nmovl rax,0x3 ;; Save the arguments count in rax\nmovl rbx,0x1 ;; Save the formal parameters count in rbx\nmovq r10,0x564ed7fdf840 ;; \ncall r10 ;; Call the ArgumentsAdaptorTrampoline"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"如你所见,不难为TF添加对参数和参数计数不匹配的支持。只需调用参数适配器trampoline即可!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"然而这种方法成本很高。对于每个优化的调用,我们现在都需要进入参数适配器trampoline,并像未优化的代码一样处理框架。这就解释了为什么在优化的代码中移除适配器框架的性能收益比在Ignition上大得多。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"但是,生成的代码非常简单。从中返回非常容易(结尾):"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"movq rsp,rbp ;; Clean callee frame\npop rbp\nret 0x8 ;; Pops a single argument (the receiver)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"我们弹出框架并根据参数计数发出一个返回指令。如果实参计数和形参计数不匹配,则适配器框架trampoline将对其进行处理。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"没有参数适配器框架的TurboFan"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"生成的代码本质上与参数计数匹配的调用代码相同。考虑调用add42(1, 2, 3)。这将生成:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"movq rdi,0x35ac082126ad ;; Load the function object \n;; Push receiver and arguments 1, 2 and 3 (reversed)\npush 0x6\npush 0x4\npush 0x2\nmovq rcx,0x35ac082030d1 ;; \npush rcx\nmovl rax,0x3 ;; Save the arguments count in rax\nmovl rcx,[rdi+0x17] ;; Load function object {Code} field in rcx\ncall rcx ;; Finally, call the code object!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"该函数的结尾如何?我们不再回到参数适配器trampoline了,因此结尾确实比以前复杂了一些。"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"movq rcx,[rbp-0x18] ;; Load the argument count (from callee frame) to rcx\nmovq rsp,rbp ;; Pop out callee frame\npop rbp\ncmpq rcx,0x0 ;; Compare arguments count with formal parameter count\njg 0x35ac000840c6 \n;; If arguments count is smaller (or equal) than the formal parameter count:\nret 0x8 ;; Return as usual (parameter count is statically known)\n;; If we have more arguments in the stack than formal parameters:\npop r10 ;; Save the return address\nleaq rsp,[rsp+rcx*8+0x8] ;; Pop all arguments according to rcx\npush r10 ;; Recover the return address\nretl"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"小结"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":"参数适配器框架是一个临时解决方案,用于实际参数和形式参数计数不匹配的调用。这是一个简单的解决方案,但它带来了很高的性能成本,并增加了代码库的复杂性。如今,许多Web框架使用这一特性来创建更灵活的API,结果带来了更高的性能成本。反转栈中参数这个简单的想法可以大大降低实现复杂性,并消除了此类调用的几乎所有开销。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}},{"type":"strong"}],"text":"原文链接:"},{"type":"link","attrs":{"href":"https:\/\/v8.dev\/blog\/adaptor-frame","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/v8.dev\/blog\/adaptor-frame"}],"marks":[{"type":"color","attrs":{"color":"#494949","name":"user"}}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章