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