前沿拓展:
參數(shù)適配器機制不僅復雜,而且成本很高。
本文最初發(fā)表于 v8.dev(Faster JavaScript calls),基于 CC 3.0 協(xié)議分享,由 InfoQ 翻譯并發(fā)布。
JavaScript 允許使用與預期形式參數(shù)數(shù)量不同的實際參數(shù)來調(diào)用一個函數(shù),也就是傳遞的實參可以少于或者多于聲明的形參數(shù)量。前者稱為申請不足(under-application),后者稱為申請過度(over-application)。
在申請不足的情況下,剩余形式參數(shù)會被分配 undefined 值。在申請過度的情況下,可以使用 rest 參數(shù)和 arguments 屬性訪問剩余實參,或者如果它們是多余的可以直接忽略。如今,許多 Web/Node.js 框架都使用這個 JS 特性來接受可選形參,并創(chuàng)建更靈活的 API。
直到最近,V8 都有一種專門的機制來處理參數(shù)大小不匹配的情況:這種機制叫做參數(shù)適配器框架。不幸的是,參數(shù)適配是有性能成本的,但在現(xiàn)代的前端和中間件框架中這種成本往往是必須的。但事實證明,我們可以通過一個巧妙的技巧來拿掉這個多余的框架,簡化 V8 代碼庫并消除幾乎所有的開銷。
我們可以通過一個**基準測試來計算移除參數(shù)適配器框架可以獲得的性能收益。
console.time();
function f(x, y, z) {}
for (let i = 0; i < N; i++) {
f(1, 2, 3, 4, 5);
}
console.timeEnd();
移除參數(shù)適配器框架的性能收益,通過一個微基準測試來得出。
上圖顯示,在無 JIT 模式(Ignition)下運行時,開銷消失,并且性能提高了 11.2%。使用 TurboFan 時,我們的速度提高了 40%。
這個微基準測試自然是為了最大程度地展現(xiàn)參數(shù)適配器框架的影響而設計的。但是,我們也在許多基準測試中看到了顯著的改進,例如我們內(nèi)部的 JSTests/Array 基準測試(7%)和 Octane2(Richards 子項為 4.6%,EarleyBoyer 為 6.1%)。
太長不看版:反轉(zhuǎn)參數(shù)
這個項目的重點是移除參數(shù)適配器框架,這個框架在訪問棧中被調(diào)用者的參數(shù)時為其提供了一個一致的接口。為此,我們需要反轉(zhuǎn)棧中的參數(shù),并在被調(diào)用者框架中添加一個包含實際參數(shù)計數(shù)的新插槽。下圖顯示了更改前后的典型框架示例。
移除參數(shù)適配器框架之前和之后的典型 JavaScript ??蚣堋?/p>
加快 JavaScript 調(diào)用
為了講清楚我們?nèi)绾渭涌煺{(diào)用,第一我們來看看 V8 如何執(zhí)行一個調(diào)用,以及參數(shù)適配器框架如何工作。
當我們在 JS 中調(diào)用一個函數(shù)調(diào)用時,V8 內(nèi)部會發(fā)生什么呢?用以下 JS 腳本為例:
function add42(x) {
return x + 42;
}
add42(3);
在函數(shù)調(diào)用期間 V8 內(nèi)部的執(zhí)行流程。
Ignition
V8 是一個多層 VM。它的第一層稱為 Ignition,是一個具有累加器寄存器的字節(jié)碼棧機。V8 第一會將代碼編譯為 Ignition 字節(jié)碼。上面的調(diào)用被編譯為以下內(nèi)容:
0d LdaUndefined ;; Load undefined into the accumulator
26 f9 Star r2 ;; Store it in register r2
13 01 00 LdaGlobal [1] ;; Load global pointed by const 1 (add42)
26 fa Star r1 ;; Store it in register r1
0c 03 Lda**i [3] ;; Load **all integer 3 into the accumulator
26 f8 Star r3 ;; Store it in register r3
5f fa f9 02 CallNoFeedback r1, r2-r3 ;; Invoke call
調(diào)用的第一個參數(shù)通常稱為接收器(receiver)。接收器是 JSFunction 中的 this 對象,并且每個 JS 函數(shù)調(diào)用都必須有一個 this。CallNoFeedback 的字節(jié)碼處理器需要使用寄存器列表 r2-r3 中的參數(shù)來調(diào)用對象 r1。
在深入研究字節(jié)碼處理器之前,請先注意寄存器在字節(jié)碼中的編碼方式。它們是負的單字節(jié)整數(shù):r1 編碼為 fa,r2 編碼為 f9,r3 編碼為 f8。我們可以將任何寄存器 ri 稱為 fb – i,實際上正如我們所見,正確的編碼是- 2 – kFixedFrameHeaderSize – i。寄存器列表使用第一個寄存器和列表的大小來編碼,因此 r2-r3 為 f9 02。
Ignition 中有許多字節(jié)碼調(diào)用處理器??梢栽诖颂幉榭此鼈兊?/span>列表。它們彼此之間略有不同。有些字節(jié)碼針對 undefined 的接收器調(diào)用、屬性調(diào)用、具有固定數(shù)量的參數(shù)調(diào)用或通用調(diào)用進行了優(yōu)化。在這里我們分析 CallNoFeedback,這是一個通用調(diào)用,在該調(diào)用中我們不會積累執(zhí)行過程中的反饋。
這個字節(jié)碼的處理器非常簡單。它是用 CodeStubAssembler 編寫的,你可以在此處查看。本質(zhì)上,它會尾調(diào)用一個架構(gòu)依賴的內(nèi)置 InterpreterPushArgsThenCall。
這個內(nèi)置方法實際上是將返回地址彈出到一個臨時寄存器中,壓入所有參數(shù)(包括接收器),第二壓回該返回地址。此時,我們不知道被調(diào)用者是否是可調(diào)用對象,也不知道被調(diào)用者期望多少個參數(shù),也就是它的形式參數(shù)數(shù)量。
內(nèi)置 InterpreterPushArgsThenCall 執(zhí)行后的框架狀態(tài)。
最終,執(zhí)行會尾調(diào)用到內(nèi)置的 Call。它會在那里檢查目標是否是適當?shù)暮瘮?shù)、構(gòu)造器或任何可調(diào)用對象。它還會讀取共享 shared function info 結(jié)構(gòu)以獲得其形式參數(shù)計數(shù)。
如果被調(diào)用者是一個函數(shù)對象,它將對內(nèi)置的 CallFunction 進行尾部調(diào)用,并在其中進行一系列檢查,包括是否有 undefined 對象作為接收器。如果我們有一個 undefined 或 null 對象作為接收器,則應根據(jù) ECMA 規(guī)范對其修補,以引用全局**對象。
執(zhí)行隨后會對內(nèi)置的 InvokeFunctionCode 進行尾調(diào)用。在沒有參數(shù)不匹配的情況下,InvokeFunctionCode 只會調(diào)用被調(diào)用對象中字段 Code 所指向的內(nèi)容。這可以是一個優(yōu)化函數(shù),也可以是內(nèi)置的 InterpreterEntryTrampoline。
如果我們假設要調(diào)用的函數(shù)尚未優(yōu)化,則 Ignition trampoline 將設置一個 IntepreterFrame。你可以在此處查看V8 中框架類型的簡短摘要。
接下來發(fā)生的事情就不用多談了,我們可以看一個被調(diào)用者執(zhí)行期間的解釋器框架快照。
我們看到框架中有固定數(shù)量的插槽:返回地址、前一個框架指針、上下文、我們正在執(zhí)行的當前函數(shù)對象、該函數(shù)的字節(jié)碼數(shù)組以及我們當前正在執(zhí)行的字節(jié)碼偏移量。最后,我們有一個專用于此函數(shù)的寄存器列表(你可以將它們視為函數(shù)局部變量)。add42 函數(shù)實際上沒有任何寄存器,但是調(diào)用者具有類似的框架,其中包含 3 個寄存器。
如預期的那樣,add42 是一個簡單的函數(shù):
25 02 Ldar a0 ;; Load the first argument to the accumulator
40 2a 00 Add**i [42] ;; Add 42 to it
ab Return ;; Return the accumulator
請注意我們在 Ldar(Load Accumulator Register)字節(jié)碼中編碼參數(shù)的方式:參數(shù) 1(a0)用數(shù)字 02 編碼。實際上,任何參數(shù)的編碼規(guī)則都是[ai] = 2 + parameter_count – i – 1,接收器[this] = 2 + parameter_count,或者在本例中[this] = 3。此處的參數(shù)計數(shù)不包括接收器。
現(xiàn)在我們就能理解為什么用這種方式對寄存器和參數(shù)進行編碼。它們只是表示一個框架指針的偏移量。第二,我們可以用相同的方式處理參數(shù)/寄存器的加載和存儲??蚣苤羔樀淖詈笠粋€參數(shù)偏移量為 2(先前的框架指針和返回地址)。這就解釋了編碼中的 2。解釋器框架的固定部分是 6 個插槽(4 個來自框架指針),因此寄存器零位于偏移量-5 處,也就是 fb,寄存器 1 位于 fa 處。很聰明是吧?
但請注意,為了能夠訪問參數(shù),該函數(shù)必須知道棧中有多少個參數(shù)!無論有多少參數(shù),索引 2 都指向最后一個參數(shù)!
Return 的字節(jié)碼處理器將調(diào)用內(nèi)置的 LeaveInterpreterFrame 來完成。該內(nèi)置函數(shù)本質(zhì)上是從框架中讀取函數(shù)對象以獲取參數(shù)計數(shù),彈出當前框架,恢復框架指針,將返回地址保存在一個暫存器中,根據(jù)參數(shù)計數(shù)彈出參數(shù)并跳轉(zhuǎn)到暫存器中的地址。
這套流程很棒!但是,當我們調(diào)用一個實參數(shù)量少于或多于其形參數(shù)量的函數(shù)時,會發(fā)生什么呢?這個聰明的參數(shù)/寄存器訪問流程將失敗,我們該如何在調(diào)用結(jié)束時清理參數(shù)?
參數(shù)適配器框架
現(xiàn)在,我們使用更少或更多的實參來調(diào)用 add42:
add42();
add42(1, 2, 3);
JS 開發(fā)人員會知道,在第一種情況下,x 將被分配 undefined,并且該函數(shù)將返回 undefined + 42 = NaN。在第二種情況下,x 將被分配 1,函數(shù)將返回 43,其余參數(shù)將被忽略。請注意,調(diào)用者不知道是否會發(fā)生這種情況。即使調(diào)用者檢查了參數(shù)計數(shù),被調(diào)用者也可以使用 rest 參數(shù)或 arguments 對象訪問其他所有參數(shù)。實際上,在 sloppy 模式下甚至可以在 add42 外部訪問 arguments 對象。
如果我們執(zhí)行與之前相同的步驟,則將第一調(diào)用內(nèi)置的 InterpreterPushArgsThenCall。它將像這樣將參數(shù)推入棧:
內(nèi)置 InterpreterPushArgsThenCall 執(zhí)行后的框架狀態(tài)。
繼續(xù)與以前相同的過程,我們檢查被調(diào)用者是否為函數(shù)對象,獲取其參數(shù)計數(shù),并將接收器補到全局**。最終,我們到達了 InvokeFunctionCode。
在這里我們不會跳轉(zhuǎn)到被調(diào)用者對象中的 Code。我們檢查參數(shù)大小和參數(shù)計數(shù)之間是否存在不匹配,第二跳轉(zhuǎn)到 ArgumentsAdaptorTrampoline。
在這個內(nèi)置組件中,我們構(gòu)建了一個額外的框架,也就是臭名昭著的參數(shù)適配器框架。這里我不會解釋內(nèi)置組件內(nèi)部發(fā)生了什么,只會向你展示內(nèi)置組件調(diào)用被調(diào)用者的 Code 之前的框架狀態(tài)。請注意,這是一個正確的 x64 call(不是 jmp),在被調(diào)用者執(zhí)行之后,我們將返回到 ArgumentsAdaptorTrampoline。這與進行尾調(diào)用的 InvokeFunctionCode 正好相反。
我們創(chuàng)建了另一個框架,該框架**了所有必需的參數(shù),以便在被調(diào)用者框架頂部精確地包含參數(shù)的形參計數(shù)。它創(chuàng)建了一個被調(diào)用者函數(shù)的接口,因此后者無需知道參數(shù)數(shù)量。被調(diào)用者將始終能夠使用與以前相同的計算結(jié)果來訪問其參數(shù),即[ai] = 2 + parameter_count – i – 1。
V8 具有一些特殊的內(nèi)置函數(shù),它們在需要通過 rest 參數(shù)或 arguments 對象訪問其余參數(shù)時能夠理解適配器框架。它們始終需要檢查被調(diào)用者框架頂部的適配器框架類型,第二采取相應措施。
如你所見,我們解決了參數(shù)/寄存器訪問問題,但是卻添加了很多復雜性。需要訪問所有參數(shù)的內(nèi)置組件都需要了解并檢查適配器框架的存在。不僅如此,我們還需要注意不要訪問過時的舊數(shù)據(jù)??紤]對 add42 的以下更改:
function add42(x) {
x += 42;
return x;
}
現(xiàn)在,字節(jié)碼數(shù)組為:
25 02 Ldar a0 ;; Load the first argument to the accumulator
40 2a 00 Add**i [42] ;; Add 42 to it
26 02 Star a0 ;; Store accumulator in the first argument slot
ab Return ;; Return the accumulator
如你所見,我們現(xiàn)在修改 a0。因此,在調(diào)用 add42(1, 2, 3)的情況下,參數(shù)適配器框架中的插槽將被修改,但調(diào)用者框架仍將包含數(shù)字 1。我們需要注意,參數(shù)對象正在訪問修改后的值,而不是舊值。
從函數(shù)返回很簡單,只是會很慢。還記得 LeaveInterpreterFrame 做什么嗎?它基本上會彈出被調(diào)用者框架和參數(shù),直到到達最大形參計數(shù)為止。因此,當我們返回參數(shù)適配器存根時,棧如下所示:
被調(diào)用者 add42 執(zhí)行之后的框架狀態(tài)。
我們需要彈出參數(shù)數(shù)量,彈出適配器框架,根據(jù)實際參數(shù)計數(shù)彈出所有參數(shù),第二返回到調(diào)用者執(zhí)行。
簡單小編綜合來說:
Deno 2020 年大事記-InfoQ
關注我并轉(zhuǎn)發(fā)此篇文章,即可獲得學習資料~若想了解更多,也可移步InfoQ官網(wǎng),獲取InfoQ最新資訊~
拓展知識:
recover4all注冊碼
http://www.sz1001.net/soft/6407.htm
Recover4all Professional v2.26 注冊機
本回答被提問者采納
recover4all注冊碼
姓 名:Legal User
注冊碼:GTNH<BHGBTZKK去下個破解版也得 http://www.ankty.com/soft/1/269/3938.html
希望對您有幫助!
recover4all注冊碼
直接到這里下載:.cn/n/netdisk/Maildir/root/tools//Recover4all.rar
recover4all注冊碼
Recover4all Pro 2.23 **注冊版
到網(wǎng)上搜一下~~~這個是破解版的
原創(chuàng)文章,作者:九賢生活小編,如若轉(zhuǎn)載,請注明出處:http://www.drmqd.com.cn/74695.html