拋棄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"}}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章