新聞中心
介紹
本文的會介紹一些關于V8內(nèi)基于推測的優(yōu)化的技術,以此來告訴大家,為什么需要TypeScript。

成都創(chuàng)新互聯(lián)公司是一家集網(wǎng)站建設,師宗企業(yè)網(wǎng)站建設,師宗品牌網(wǎng)站建設,網(wǎng)站定制,師宗網(wǎng)站建設報價,網(wǎng)絡營銷,網(wǎng)絡優(yōu)化,師宗網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強企業(yè)競爭力。可充分滿足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時我們時刻保持專業(yè)、時尚、前沿,時刻以成就客戶成長自我,堅持不斷學習、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實用型網(wǎng)站。
我們將以一段函數(shù)的執(zhí)行未展開,從函數(shù)執(zhí)行的角度來看看,一段代碼如何被執(zhí)行,優(yōu)化,再最后,你會了解,為什么TypeScript更好。
看完本文后,你不需要記住文章中出現(xiàn)的繁雜的指令和代碼,只需要在你的腦海中存在一個印象,避免寫出糟糕的代碼,以及,盡量使用TypeScript。
如何執(zhí)行代碼?
作為介紹的第一部分,我們會用一段簡短的篇幅帶大家看看,你的代碼如何被執(zhí)行
當然,如果用簡單的流程圖表示,你可以把上面的過程理解為這樣一個線性的執(zhí)行過程,當然可能并不嚴謹,稍后我們會繼續(xù)介紹。
下面讓我們從一段具體的代碼來看一下這個過程。
一段簡單的代碼?
function add(x, y) {
return x + y;
}
console.log(add(1, 2))
如果你在chrome的DevTools console中運行這段代碼,你可以看到預期的輸出值3。
根據(jù)上面的流程圖,這段代碼被執(zhí)行的第一步,是被解析器解析為AST,這一步我們用d8 shell 的Debug版本中使用 –print-ast 命令來查看V8內(nèi)部生成的AST。
$ out/Debug/d8 --print-ast add.js
…-
-- AST ---
FUNC at 12
. KIND 0
. SUSPEND COUNT 0
. NAME "add"
. PARAMS
. . VAR (0x7fbd5e818210) (mode = VAR) "x"
. . VAR (0x7fbd5e818240) (mode = VAR) "y"
. RETURN at 23
. . ADD at 32
. . . VAR PROXY parameter[0] (0x7fbd5e818210) (mode = VAR) "x"
. . . VAR PROXY parameter[1] (0x7fbd5e818240) (mode = VAR) "y
很多人可能或多或少接觸過AST的概念,這里不多贅述,只是用一張簡單的圖表示下上面的過程。
最開始,函數(shù)字面量add被解析為樹形表示,其中一個子樹用于參數(shù)聲明,另外一個子樹用于實際的的函數(shù)體。在解析階段,不可能知道程序中名稱和變量的綁定關系,這主要是因為“有趣的變量聲明提升規(guī)則”以及JavaScript中的eval,此外還有其他原因。
一旦我們構建完成了AST,它便包含了從中生成可執(zhí)行字節(jié)碼的所有必要信息。AST隨后被傳遞給BytecodeGenerator ,BytecodeGenerator 是屬于Ignition 的一部分,它以函數(shù)為單位生成字節(jié)碼(_其他引擎并不一定以函數(shù)為單位生成的_)。你也可以在d8中使用命令–print-bytecode來查看V8生成的字節(jié)碼(或者用node端)
$ out/Debug/d8 --print-bytecode add.js
…[
generated bytecode for function: add]
Parameter count 3
Frame size 0
12 E> 0x37738712a02a @ 0 : 94 StackCheck
23 S> 0x37738712a02b @ 1 : 1d 02 Ldar a1
32 E> 0x37738712a02d @ 3 : 29 03 00 Add a0, [0]
36 S> 0x37738712a030 @ 6 : 98 Return
Constant pool (size = 0)
Handler Table (size = 16)
上面過程中為函數(shù)add生成了一個新的字節(jié)碼對象,它接受三個參數(shù),一個內(nèi)部的this引用,以及兩個顯式形參x和y。該函數(shù)不需要任何的局部變量(所以棧幀大小為0),并且包含下面這四個字節(jié)碼指令組成的序列
StackCheck
Ldar a1
Add a0, [0]
Return
為了解釋這段字節(jié)碼,我們首先需要從較高的層面來認知解釋器如何工作。V8的解釋器是基于寄存器架構(register machine)的(相對的是基于棧架構,也是早期V8版本中使用的 FullCodegen 編譯器)。Ignition 會把指令序列都保存在解釋器自身的(虛擬)寄存器中,這些寄存器部分被映射到實際CPU的寄存器中,而另外一部分會用實際機器的棧內(nèi)存來模擬。
有兩個特殊寄存器a0和a1對應著函數(shù)在機器棧(即內(nèi)存棧)上的形式參數(shù)(在函數(shù)add這個例子中,有兩個形參)。形參是在源代碼中聲名的參數(shù),它可能與在運行時傳遞給函數(shù)的實際參數(shù)數(shù)量不同。每個字節(jié)碼指令執(zhí)行得到的最終值通常被保存在一個稱作累加器(accumulator)的特殊寄存器中。堆棧指針(stack pointer )指向當前的棧幀或者說激活記錄,程序計數(shù)器( program counter)指向指令字節(jié)碼中當前正在執(zhí)行的指令。下面我們看看這個例子中每條字節(jié)碼指令都做了什么。
- StackCheck 會將堆棧指針與一些已知的上限比較(實際上在V8中應該稱作下限,因為棧是從高地址到低地址向下生長的)。如果棧的增長超過了某個閾值,就會放棄函數(shù)的執(zhí)行,同時拋出一個 RangeError 來告訴我們棧溢出了。
- Ldar a1將寄存器a1的值加載到累加器寄存器中(Ladr 表示 LoaD Accumulator Register)
- Add a0, [0] 讀取寄存器a0里的值,并把它加到累加器的值上。結果被再次放到累加器中。
為什么這條指令是這樣的?以一條JS 語句為例
var dest = src1 + src2 // op dest, src1,src2
var dest += src; //op dest, src
+src; // op src
分別表示三地址指令,二地址指令,一地址指令,我在后面分別標注了轉(zhuǎn)換后的機器指令。三地址和二地址指令都指定了運算后儲存結果的位置。
但在一地址指令中,沒有指定目標源。實際上,它會被默認存在一個累加器”(accumulator)的專用寄存器,保存計算結果。
其中Add運算符的[0]操作數(shù)指向一個「反饋向量槽( feedback vector slot)」,它是解釋器用來儲存有關函數(shù)執(zhí)行期間看到的值的分析信息。在后面講解TurboFan 如何優(yōu)化函數(shù)的時候會再次回到這。
- Return 結束當前函數(shù)的執(zhí)行,并把控制權交給調(diào)用者。返回值是累加器中的當前值。
當最終生成了上面這段字節(jié)碼后,會被送入的VM ,一般會由解釋器進行執(zhí)行,這種執(zhí)行方式是最原始也是效率最低的。我們可以在下一部分了解到,這種原始的執(zhí)行會經(jīng)歷什么。
關于字節(jié)碼的解釋,這里不會做過多的贅述,如果你感興趣,可以擴展閱讀 「Understanding V8’s Bytecode」 (https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775) 一文,這篇字節(jié)碼對V8的字節(jié)碼的工作原理提供了一些深入的了解。
為什么需要優(yōu)化?
現(xiàn)在,我相信你已經(jīng)對V8如何執(zhí)行一段代碼有了一個簡單的認識。在正式進入我們的主題之前,還需要解釋一個很關鍵的問題,為什么我們需要優(yōu)化。為了回答這個問題,我們需要先看下下規(guī)范
關于規(guī)范的更多了解,可以去這里查找 https://tc39.es/ecma262/#sec-toprimitive
我們再以看最常見的 ToPrimitive 為例,需要經(jīng)過非常繁瑣的求值過程,而這些過程都是為了解決操作類型的動態(tài)性
在JavaScript中的“+”運算符已經(jīng)是一個相當復雜的操作了,在最終執(zhí)行一個數(shù)值相加之前必須進行大量的檢查
而如果引擎要想讓這些步驟是能夠在幾個機器指令內(nèi)完成以達到峰值性能(與C++相媲美),這里有一個關鍵能力—-推測優(yōu)化,通過假設可能的輸入。例如,當我們知道表達式x+y中,x和y都是數(shù)字,那么我們就不需要處理任何一個是字符串或者其他更糟糕的情況—-操作數(shù)是任意類型的JavaScript對象,也就不需要對所有參數(shù)調(diào)用一遍 ToPrimitive 了。
換句話說,如果我們能夠確定x,y 都是數(shù)字類型,我們自然就很容易對這個函數(shù)執(zhí)行進行優(yōu)化,消除冗余的IR指令。
「而從執(zhí)行的角度來說,動態(tài)類型性能瓶頸很大程度是因為它的動態(tài)的類型系統(tǒng),與靜態(tài)類型的語言相比,JavaScript 程序需要額外的操作來處理類型的動態(tài)性,所以執(zhí)行效率比較低?!?/strong>
那么如何確認x,y都是數(shù)字,我們又如何優(yōu)化呢?
基于推測的優(yōu)化?
因為 JavaScript 動態(tài)語言的特性,我們通常直到運行時才知道值的確切類型,僅僅觀察源代碼,往往不可能知道某個操作的可能輸入值。所以這就是為什么我們需要推測,根據(jù)之前運行收集到的值的反饋,然后假設將來總會看到類似的值。這種方法聽起來可能作用相當有限,但它已被證明適用于JavaScript這樣的動態(tài)語言。
你可能會聯(lián)想到CPU的分支預測能力,如果是這樣,那么恭喜你,你并沒有想錯。
我們再回到這段代碼
function add(x, y) {
return x + y;
}
你可能已經(jīng)想到了,作為一個動態(tài)類型的語言,推測的第一步,就是要收集到足夠多的信息,來預測 ??add?? 在今后的執(zhí)行中會遇到的類型。
所以,首先向你介紹反饋向量(Feedback Vector),它是我們執(zhí)行預測最核心的成員之一:負責儲存我們收集到的信息。
反饋向量(Feedback Vector)
當一段代碼被初次執(zhí)行時,它所執(zhí)行的往往是解釋器產(chǎn)生的字節(jié)碼。當這段字節(jié)碼每次的執(zhí)行后,都會會產(chǎn)生一些反饋信息,這些反饋信息會被儲存在「反饋向量」(過去叫類型反饋向量) 中,這個特殊的數(shù)據(jù)結構會被鏈接在閉包上。如果從對象結構的角度來看,反饋向量和其他相關的內(nèi)容會是這樣。
其中 SharedFunctionInfo,它包含了函數(shù)的一般信息,比如源位置,字節(jié)碼,嚴格或一般模式。除此之外,還有一個指向上下文的指針,其中包含自由變量的值以及對全局對象的訪問。
關于自由變量和約束變量的概念, 閉包 (計算機科學)
反饋向量的大致結構如下,slot是一個槽,表示向量表里面的一項,包含了操作類型和傳入的值類型,
|
IC Slot |
IC Type |
Value |
|
1 |
Call |
UNINIT |
|
2 |
BinaryOp |
SignedSmall |
比如,第二個是一個 BinaryOp 槽,二元操作符類似“+,-”等能夠記錄迄今為止看到的輸入和輸出的反饋。先不用糾結它的含義,后面我們會具體介紹。
如果你想查看你的函數(shù)所對應的反饋向量,可以在你的代碼中加上專門的內(nèi)部函數(shù) ??%DebugPrint??? ,并且在d8中加上命令 ??–allow-natives-syntax?? 來檢查特定閉包的反饋向量的內(nèi)容。
源代碼:
function add(x, y) {
return x + y;
}
console.log(add(1, 2));
%DebugPrint(add);
在d8 使用這個命令 –allow-natives-syntax 運行,我們看到 :
$ out/Debug/d8 --allow-natives-syntax add.js
DebugPrint: 0xb5101ea9d89: [Function] in OldSpace
- feedback vector: 0xb5101eaa091: [FeedbackVector] in OldSpace
- length: 1
SharedFunctionInfo: 0xb5101ea99c9
Optimized Code: 0
Invocation Count: 1
Profiler Ticks: 0
Slot #0 BinaryOp BinaryOp:SignedSmall
我們看到調(diào)用次數(shù)(Invocation Count)是1,因為我們只調(diào)用了一次函數(shù)add。此時還沒有優(yōu)化代碼(根據(jù)Optimized Code的值為0)。反饋向量的長度為1,說明里面只有一個槽,就是我們上面說到的二元操作符槽(BinaryOp Slot),當前反饋為 SignedSmall。
這個反饋SignedSmall代表什么?這表明指令Add只看到了SignedSmall類型的輸入,并且直到現(xiàn)在也只產(chǎn)生了SignedSmall類型的輸出。
但是什么是SignedSmall類型?JavaScript里面并不存在這種類型。實際上,SignedSmall來自己V8中的一種優(yōu)化策略,它表示在程序中經(jīng)常使用的小的有符號整數(shù)(V8將高位的32位表示整數(shù),低位的全部置0來表示SignedSmall),這種類型能夠獲得特殊處理(其他JavaScript引擎也有類似的優(yōu)化策略)。
「值的表示」
V8通常使用一種叫做指針標記(Pointer Tagging)的技術來表示值,應用這種技術,V8在每個值里面都設置一個標識。我們處理的大部分值都分配在JavaScript堆上,并且由垃圾回收器(GC)來管理。但是對某些值來說,總是將它們分配在內(nèi)存里開銷會很大。尤其是對于小整數(shù),它們通常會用在數(shù)組索引和一些臨時計算結果。
在V8中存在兩種指針標識類型:分別是是Smi(即 Small Integer的縮寫)和堆對象( HeapObject,就是JavaScript的引用類型),其中堆對象是分配在內(nèi)存的堆中,圖中的地址即指向堆中的某塊地方。
我們用最低有效位來區(qū)分堆對象(標志是1)和小整數(shù)(標志是0)。對于64位結構上的Smi,至少有32位有效位(低半部)是一直被置為0。另外32位,也就是Word的上半部,是被用來儲存32位有符號小整數(shù)的值。
僅僅是一次的執(zhí)行,還不足以讓引擎這么快下定決心,相信add 函數(shù)隨后的執(zhí)行都是Smi 類型。那么我們先來看看,如果在隨后的執(zhí)行中,我們傳入不一樣的類型會怎么樣。
反饋向量的變化
反饋類型SignedSmall是指所有能用小整數(shù)表示的值。對于add操作而言,這意味著目前為止它只能看到輸入類型為Smi,并且所產(chǎn)生的輸出值也都是Smi(也就是說,所有的值都沒有超過32位整數(shù)的范圍)。下面我們來看看,當我們調(diào)用add的時候傳入一個不是Smi的值會發(fā)生什么。
function add(x, y) {
return x + y;
}
console.log(add(1, 2));
console.log(add(1.1, 2.2));
//調(diào)用100ci
%DebugPrint(add);
在d8加入命令 –allow-natives-syntax ,然后看到下面結果。
$ out/Debug/d8 --allow-natives-syntax add.js
DebugPrint: 0xb5101ea9d89: [Function] in OldSpace
…
- feedback vector: 0x3fd6ea9ef9: [FeedbackVector] in OldSpace
- length: 1
SharedFunctionInfo: 0x3fd6ea9989
Optimized Code: 0
Invocation Count: 2
Profiler Ticks: 0
Slot #0 BinaryOp BinaryOp:Number
首先,我們看到調(diào)用次數(shù)現(xiàn)在是2,因為運行了兩次函數(shù)add。然后發(fā)現(xiàn)BinaryOp 槽的值現(xiàn)在變成了Number,這表明對于這個加法已經(jīng)有傳入了任意類型的數(shù)值(即非整數(shù))。此外,這有一個反饋向量的狀態(tài)遷移圖,大致如下所示:
反饋狀態(tài)從 None 開始,這表明目前還沒有看到任何輸入,所以什么都不知道。狀態(tài)Any表明我們看到了不兼容的(比如number和string)輸入和輸出的組合。狀態(tài)Any意味著Add(字節(jié)碼中的)是多態(tài)。相比之下,其余的狀態(tài)表明Add都是單態(tài)(monomorphic),因為看到的輸入和產(chǎn)生的都是相同類型的值。下面是圖中名詞解釋:
- SignedSmall 表示所有的值都是小整數(shù)(有效數(shù)值為是32位或者31位,取決于Word的在不同架構上的大?。?,均表示為Smi。
- Number 表明所有的值都常規(guī)數(shù)字 (這包括小整數(shù))。
- NumberOrOddball 包括其他能被轉(zhuǎn)換成 Number 的 undefined, null, true 和 false 。
- String :所有輸入值都是字符串
- BigInt 表示輸入都是大整數(shù)。
需要注意一點,反饋只能在這個圖中前進(從 None 到 Any),不能回退。如果真的那樣做,那么我們就會有陷入去優(yōu)化循環(huán)的風險。那樣情況下,優(yōu)化編譯器發(fā)現(xiàn)輸入值與之前得到反饋內(nèi)容不同,比如之前解釋器生成的反饋是 Number,但現(xiàn)在輸入值出現(xiàn)了 String,這時候已經(jīng)生成的反饋和優(yōu)化代碼就會失效,并回退到解釋器生成的字節(jié)碼版本。當下一次函數(shù)再次變熱(hot,多次運行),我們將再次優(yōu)化它,如果允許回退,這時候優(yōu)化編譯器會再次生成相同的代碼,這意味著會再次回到 Number 的情況。如果這樣無限制的回退去優(yōu)化,再優(yōu)化,編譯器將會忙于優(yōu)化和去優(yōu)化,而不是高速運行 JavaScript 代碼。
優(yōu)化管道(The Optimization Pipeline)
現(xiàn)在我們知道了解釋器Ignition 是如何為函數(shù)add收集反饋,下面來看看優(yōu)化編譯器如何利用反饋生成最小的代碼,因為_越小的機器指令代碼塊,意味著更快的速度_。為了觀察,我將使用一個特殊的內(nèi)部函數(shù)OptimizeFunctionOnNextCall()在特定的時間點觸發(fā)V8對函數(shù)的優(yōu)化。我們經(jīng)常使用這些內(nèi)部函數(shù)以非常特定的方式對引擎進行測試。
function add(x, y) {
return x + y;
}
add(1, 2); // Warm up with SignedSmall feedback.
%OptimizeFunctionOnNextCall(add);
add(1, 2); // Optimize and run generated code
在這里,給函數(shù)add傳遞兩個整數(shù)型值來明確call site “x + y”的反饋會被預熱為小整數(shù)(表示_這個call site全部傳遞的都是小整數(shù),對于優(yōu)化引擎來說將來得到的輸入也會是小整數(shù)_),并且結果也是屬于小整數(shù)范圍。然后我們告訴V8應該在下次調(diào)用函數(shù)add的時候去優(yōu)化它(用TurboFan ),最終再次調(diào)用add,觸發(fā)優(yōu)化編譯器運行生成機器碼。
TurboFan 拿到之前為函數(shù)add生成的字節(jié)碼,并從函數(shù)add的反饋向量表里提取出相關的反饋。優(yōu)化編譯器將這些信息轉(zhuǎn)換成一個圖表示,再將這個圖表示傳遞給前端,優(yōu)化以及后端的各個階段(見上圖)。在本文不會詳細展開這部分內(nèi)容,這是另一個系列的內(nèi)容了。我們要了解的是最終生成的機器碼,并看看優(yōu)化推測是如何工作的。你可以在d8中加上命令 –print-opt-code來查看由TurboFan 生成的優(yōu)化代碼。
這是由TurboFan 在x64架構上生成的機器碼,這里省略了一些無關緊要的技術細節(jié)(,下面就來看看這些代碼做了什么。
# Prologue
leaq rcx,[rip+0x0]
movq rcx,[rcx-0x37]
testb [rcx+0xf],0x1
jnz CompileLazyDeoptimizedCode
push rbp
movq rbp,rsp
push rsi
push rdi
cmpq rsp,[r13+0xdb0]
jna StackCheck
第一段代碼檢查對象是否仍然有效(對象的形狀是否符合之前生成機器碼的那個),或者某些條件是否發(fā)生了改變,這就需要丟棄這個優(yōu)化代碼。這部分具體內(nèi)容可以參考 Juliana Franco 的 “Internship on Laziness“。一旦我們知道這段代碼仍然有效,就會建立一個棧幀并且檢查堆棧上是否有足夠的空間來執(zhí)行代碼。
# Check x is a small integer
movq rax,[rbp+0x18]
test al,0x1
jnz Deoptimize
# Check y is a small integer
movq rbx,[rbp+0x10]
testb rbx,0x1
jnz Deoptimize
# Convert y from Smi to Word32
movq rdx,rbx
shrq rdx, 32
# Convert x from Smi to Word32
movq rcx,rax
shrq rcx, 32
然后從函數(shù)主體開始。我們從棧中讀取參數(shù)x和y的值(相對于幀指針rbp,比如rbp+1這樣的地址,請參考棧幀概念),然后檢查兩個參數(shù)是否都是 Smi 類型(因為根據(jù)“+”得到的反饋,兩個輸入總是Smi)。這一步是通過測試最低有效位來完成。一旦確定了參數(shù)都是Smi,我們需要將它轉(zhuǎn)換成32位表示,這是通過將值右移32位來完成的。如果x或y不是Smi,則會立即終止執(zhí)行優(yōu)化代碼,接著負責去優(yōu)化的模塊就會恢復到之前解釋器生成的函數(shù)add的代碼(即字節(jié)碼)。
# Add x and y (incl. overflow check)
addl rdx,rcx
jo Deoptimize
# Convert result to Smi
shlq rdx, 32
movq rax,rdx
# Epilogue
movq rsp,rbp
pop rbp
ret 0x18
然后我們繼續(xù)執(zhí)行對輸入值的整數(shù)加法,這時需要明確地測試溢出,因為加法的結果可能超出32位整數(shù)的范圍,在這種情況下就要返回到解釋器版本,并在隨后將add的反饋類型提升為Number(之前說過,反饋類型的改變只能前進)。
最后我們通過將帶符號的32位值向上移動32位,將結果轉(zhuǎn)換回Smi表示,并將結果返回存到累加器rax 。
我們現(xiàn)在可以看到生成的代碼是高度優(yōu)化的,并且適用于專門的反饋。它完全不去處理其他數(shù)字,字符串,大整數(shù)或任意JavaScript對象,只關注目前為止我們所看到的那種類型的值。這是使許多JavaScript應用程序達到最佳性能的關鍵因素。
為什么需要TypeScript?
在上面的介紹中,我們竭力避免了對JavaScript 對象的訪問,如果有對象加入,這將會變成一個很復雜的話題。但為了更好的展開這個話題,我們還是需要提一下,關于對象的優(yōu)化是V8中極其重要的一部分。例如,以下面這個對象為例
var o = {
x: ''
}
var o1 = {
x: ''
y
}
//o1. o2
對于像 o.x這樣的屬性訪問,若o始終具有相同的形狀(形狀同結構,即相同的屬性以及屬性是相同的順序,例如o的結構一直是{x:v},其中v的類型是String),我們會把如何獲得o.x的過程信息緩存起來,構造成一個隱藏類( Hidden Class)。在隨后執(zhí)行相同的字節(jié)碼時,不需要再次搜索對象o中x的位置。這種底層實現(xiàn)被稱為內(nèi)聯(lián)緩存– inline cache (IC)。
你可以在Vyacheslav Egoro寫的這篇文章 “What’s up with monomorphism?” 中了解更多關于ICs和屬性訪問的細節(jié)。
總而言之,你現(xiàn)在應該了解到,作為一門弱類型的語言,從最早的SELF和smalltalk 語言開始,研究者就在不斷去優(yōu)化這種弱類型語言的執(zhí)行效率。
「從執(zhí)行的角度來說,動態(tài)類型性能瓶頸很大程度是因為它的動態(tài)的類型系統(tǒng),與靜態(tài)類型的語言相比, JavaScript 程序需要額外的操作來處理類型的動態(tài)性,所以執(zhí)行效率比較低?!?/p>
說了這么多,最關鍵的一點
「確定你的代碼將要看到的類型很重要」
再加上另外一句話:
「作為動態(tài)語言,你的程序可能在90%的時間里,都在處理和代碼邏輯無關的事情。即:確認你的代碼是什么形狀」
從傳統(tǒng)的JavaScript 角度來說。
function add(x, y) {
return x + y;
}
你無法很好的保證 add 函數(shù)將要看到的類型,哪怕你確實想要這么做。但在一個大型的系統(tǒng)中,維護每一個函數(shù)和對象的形狀,極其困難。
你可能在前99次都保證了add 看到的都是Smi 類型,但是在第100次,add 看到了一個String,而在這之前,優(yōu)化編輯器,即TurboFan,已經(jīng)大膽的推測了你的函數(shù)只會看到Smi,那么這時候
Ops!
優(yōu)化編輯器將不得不認為自己做了個錯誤的預測,它會立即把之前的優(yōu)化丟掉。從字節(jié)碼開始重新執(zhí)行。
而如果你的代碼一直陷入優(yōu)化<->去優(yōu)化的怪圈,那么程序執(zhí)行將會變慢,慢到還不如不優(yōu)化。
大多數(shù)的瀏覽器都做了限制,當優(yōu)化/去優(yōu)化循環(huán)發(fā)生的時候會嘗試跳出這種循環(huán)。比如,如果 JIT 做了 10 次以上的優(yōu)化并且又丟棄的操作,那么就不繼續(xù)嘗試去優(yōu)化這段代碼。
所以,到這里你應該明白了,有兩點準則:
- 「確保你的代碼是什么形狀很重要」
但比第一條更重要的是:
- 「確保你的代碼固定在某個形狀上」
而編寫TypeScript ,從工程和語言的層面上幫助你解決了這兩個準則,你可以暢快的使用TypeScript,而無需擔心你是否不小心違背了上面兩條準則。
當前題目:V8引擎:基于類型推測的性能優(yōu)化原理
網(wǎng)址分享:http://fisionsoft.com.cn/article/djgjheo.html


咨詢
建站咨詢
