V8 引擎:基于類(lèi)型推測(cè)的性能優(yōu)化原理
介紹
本文的會(huì)介紹一些關(guān)于V8內(nèi)基于推測(cè)的優(yōu)化的技術(shù),以此來(lái)告訴大家,為什么需要TypeScript。
我們將以一段函數(shù)的執(zhí)行未展開(kāi),從函數(shù)執(zhí)行的角度來(lái)看看,一段代碼如何被執(zhí)行,優(yōu)化,再最后,你會(huì)了解,為什么TypeScript更好。
看完本文后,你不需要記住文章中出現(xiàn)的繁雜的指令和代碼,只需要在你的腦海中存在一個(gè)印象,避免寫(xiě)出糟糕的代碼,以及,盡量使用TypeScript。
如何執(zhí)行代碼?
作為介紹的第一部分,我們會(huì)用一段簡(jiǎn)短的篇幅帶大家看看,你的代碼如何被執(zhí)行
當(dāng)然,如果用簡(jiǎn)單的流程圖表示,你可以把上面的過(guò)程理解為這樣一個(gè)線性的執(zhí)行過(guò)程,當(dāng)然可能并不嚴(yán)謹(jǐn),稍后我們會(huì)繼續(xù)介紹。
下面讓我們從一段具體的代碼來(lái)看一下這個(gè)過(guò)程。
一段簡(jiǎn)單的代碼?
如果你在chrome的DevTools console中運(yùn)行這段代碼,你可以看到預(yù)期的輸出值3。
根據(jù)上面的流程圖,這段代碼被執(zhí)行的第一步,是被解析器解析為AST,這一步我們用d8 shell 的Debug版本中使用 –print-ast 命令來(lái)查看V8內(nèi)部生成的AST。
很多人可能或多或少接觸過(guò)AST的概念,這里不多贅述,只是用一張簡(jiǎn)單的圖表示下上面的過(guò)程。
最開(kāi)始,函數(shù)字面量add被解析為樹(shù)形表示,其中一個(gè)子樹(shù)用于參數(shù)聲明,另外一個(gè)子樹(shù)用于實(shí)際的的函數(shù)體。在解析階段,不可能知道程序中名稱(chēng)和變量的綁定關(guān)系,這主要是因?yàn)椤坝腥さ淖兞柯暶魈嵘?guī)則”以及JavaScript中的eval,此外還有其他原因。
一旦我們構(gòu)建完成了AST,它便包含了從中生成可執(zhí)行字節(jié)碼的所有必要信息。AST隨后被傳遞給BytecodeGenerator ,BytecodeGenerator 是屬于Ignition 的一部分,它以函數(shù)為單位生成字節(jié)碼(_其他引擎并不一定以函數(shù)為單位生成的_)。你也可以在d8中使用命令–print-bytecode來(lái)查看V8生成的字節(jié)碼(或者用node端)
上面過(guò)程中為函數(shù)add生成了一個(gè)新的字節(jié)碼對(duì)象,它接受三個(gè)參數(shù),一個(gè)內(nèi)部的this引用,以及兩個(gè)顯式形參x和y。該函數(shù)不需要任何的局部變量(所以棧幀大小為0),并且包含下面這四個(gè)字節(jié)碼指令組成的序列
為了解釋這段字節(jié)碼,我們首先需要從較高的層面來(lái)認(rèn)知解釋器如何工作。V8的解釋器是基于寄存器架構(gòu)(register machine)的(相對(duì)的是基于棧架構(gòu),也是早期V8版本中使用的 FullCodegen 編譯器)。Ignition 會(huì)把指令序列都保存在解釋器自身的(虛擬)寄存器中,這些寄存器部分被映射到實(shí)際CPU的寄存器中,而另外一部分會(huì)用實(shí)際機(jī)器的棧內(nèi)存來(lái)模擬。
有兩個(gè)特殊寄存器a0和a1對(duì)應(yīng)著函數(shù)在機(jī)器棧(即內(nèi)存棧)上的形式參數(shù)(在函數(shù)add這個(gè)例子中,有兩個(gè)形參)。形參是在源代碼中聲名的參數(shù),它可能與在運(yùn)行時(shí)傳遞給函數(shù)的實(shí)際參數(shù)數(shù)量不同。每個(gè)字節(jié)碼指令執(zhí)行得到的最終值通常被保存在一個(gè)稱(chēng)作累加器(accumulator)的特殊寄存器中。堆棧指針(stack pointer )指向當(dāng)前的棧幀或者說(shuō)激活記錄,程序計(jì)數(shù)器( program counter)指向指令字節(jié)碼中當(dāng)前正在執(zhí)行的指令。下面我們看看這個(gè)例子中每條字節(jié)碼指令都做了什么。
- StackCheck 會(huì)將堆棧指針與一些已知的上限比較(實(shí)際上在V8中應(yīng)該稱(chēng)作下限,因?yàn)闂J菑母叩刂返降偷刂废蛳律L(zhǎng)的)。如果棧的增長(zhǎng)超過(guò)了某個(gè)閾值,就會(huì)放棄函數(shù)的執(zhí)行,同時(shí)拋出一個(gè) RangeError 來(lái)告訴我們棧溢出了。
- Ldar a1將寄存器a1的值加載到累加器寄存器中(Ladr 表示 LoaD Accumulator Register)
- Add a0, [0] 讀取寄存器a0里的值,并把它加到累加器的值上。結(jié)果被再次放到累加器中。
為什么這條指令是這樣的?以一條JS 語(yǔ)句為例
分別表示三地址指令,二地址指令,一地址指令,我在后面分別標(biāo)注了轉(zhuǎn)換后的機(jī)器指令。三地址和二地址指令都指定了運(yùn)算后儲(chǔ)存結(jié)果的位置。
但在一地址指令中,沒(méi)有指定目標(biāo)源。實(shí)際上,它會(huì)被默認(rèn)存在一個(gè)累加器”(accumulator)的專(zhuān)用寄存器,保存計(jì)算結(jié)果。
其中Add運(yùn)算符的[0]操作數(shù)指向一個(gè)「反饋向量槽( feedback vector slot)」,它是解釋器用來(lái)儲(chǔ)存有關(guān)函數(shù)執(zhí)行期間看到的值的分析信息。在后面講解TurboFan 如何優(yōu)化函數(shù)的時(shí)候會(huì)再次回到這。
- Return 結(jié)束當(dāng)前函數(shù)的執(zhí)行,并把控制權(quán)交給調(diào)用者。返回值是累加器中的當(dāng)前值。
當(dāng)最終生成了上面這段字節(jié)碼后,會(huì)被送入的VM ,一般會(huì)由解釋器進(jìn)行執(zhí)行,這種執(zhí)行方式是最原始也是效率最低的。我們可以在下一部分了解到,這種原始的執(zhí)行會(huì)經(jīng)歷什么。
關(guān)于字節(jié)碼的解釋?zhuān)@里不會(huì)做過(guò)多的贅述,如果你感興趣,可以擴(kuò)展閱讀 「Understanding V8’s Bytecode」 (https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775) 一文,這篇字節(jié)碼對(duì)V8的字節(jié)碼的工作原理提供了一些深入的了解。
為什么需要優(yōu)化?
現(xiàn)在,我相信你已經(jīng)對(duì)V8如何執(zhí)行一段代碼有了一個(gè)簡(jiǎn)單的認(rèn)識(shí)。在正式進(jìn)入我們的主題之前,還需要解釋一個(gè)很關(guān)鍵的問(wèn)題,為什么我們需要優(yōu)化。為了回答這個(gè)問(wèn)題,我們需要先看下下規(guī)范
關(guān)于規(guī)范的更多了解,可以去這里查找 https://tc39.es/ecma262/#sec-toprimitive
我們?cè)僖钥醋畛R?jiàn)的 ToPrimitive 為例,需要經(jīng)過(guò)非常繁瑣的求值過(guò)程,而這些過(guò)程都是為了解決操作類(lèi)型的動(dòng)態(tài)性
在JavaScript中的“+”運(yùn)算符已經(jīng)是一個(gè)相當(dāng)復(fù)雜的操作了,在最終執(zhí)行一個(gè)數(shù)值相加之前必須進(jìn)行大量的檢查
而如果引擎要想讓這些步驟是能夠在幾個(gè)機(jī)器指令內(nèi)完成以達(dá)到峰值性能(與C++相媲美),這里有一個(gè)關(guān)鍵能力—-推測(cè)優(yōu)化,通過(guò)假設(shè)可能的輸入。例如,當(dāng)我們知道表達(dá)式x+y中,x和y都是數(shù)字,那么我們就不需要處理任何一個(gè)是字符串或者其他更糟糕的情況—-操作數(shù)是任意類(lèi)型的JavaScript對(duì)象,也就不需要對(duì)所有參數(shù)調(diào)用一遍 ToPrimitive 了。
換句話說(shuō),如果我們能夠確定x,y 都是數(shù)字類(lèi)型,我們自然就很容易對(duì)這個(gè)函數(shù)執(zhí)行進(jìn)行優(yōu)化,消除冗余的IR指令。
「而從執(zhí)行的角度來(lái)說(shuō),動(dòng)態(tài)類(lèi)型性能瓶頸很大程度是因?yàn)樗膭?dòng)態(tài)的類(lèi)型系統(tǒng),與靜態(tài)類(lèi)型的語(yǔ)言相比,JavaScript 程序需要額外的操作來(lái)處理類(lèi)型的動(dòng)態(tài)性,所以執(zhí)行效率比較低。」
那么如何確認(rèn)x,y都是數(shù)字,我們又如何優(yōu)化呢?
基于推測(cè)的優(yōu)化?
因?yàn)?JavaScript 動(dòng)態(tài)語(yǔ)言的特性,我們通常直到運(yùn)行時(shí)才知道值的確切類(lèi)型,僅僅觀察源代碼,往往不可能知道某個(gè)操作的可能輸入值。所以這就是為什么我們需要推測(cè),根據(jù)之前運(yùn)行收集到的值的反饋,然后假設(shè)將來(lái)總會(huì)看到類(lèi)似的值。這種方法聽(tīng)起來(lái)可能作用相當(dāng)有限,但它已被證明適用于JavaScript這樣的動(dòng)態(tài)語(yǔ)言。
你可能會(huì)聯(lián)想到CPU的分支預(yù)測(cè)能力,如果是這樣,那么恭喜你,你并沒(méi)有想錯(cuò)。
我們?cè)倩氐竭@段代碼
你可能已經(jīng)想到了,作為一個(gè)動(dòng)態(tài)類(lèi)型的語(yǔ)言,推測(cè)的第一步,就是要收集到足夠多的信息,來(lái)預(yù)測(cè) ??add?
? 在今后的執(zhí)行中會(huì)遇到的類(lèi)型。
所以,首先向你介紹反饋向量(Feedback Vector),它是我們執(zhí)行預(yù)測(cè)最核心的成員之一:負(fù)責(zé)儲(chǔ)存我們收集到的信息。
反饋向量(Feedback Vector)
當(dāng)一段代碼被初次執(zhí)行時(shí),它所執(zhí)行的往往是解釋器產(chǎn)生的字節(jié)碼。當(dāng)這段字節(jié)碼每次的執(zhí)行后,都會(huì)會(huì)產(chǎn)生一些反饋信息,這些反饋信息會(huì)被儲(chǔ)存在「反饋向量」(過(guò)去叫類(lèi)型反饋向量) 中,這個(gè)特殊的數(shù)據(jù)結(jié)構(gòu)會(huì)被鏈接在閉包上。如果從對(duì)象結(jié)構(gòu)的角度來(lái)看,反饋向量和其他相關(guān)的內(nèi)容會(huì)是這樣。
其中 SharedFunctionInfo,它包含了函數(shù)的一般信息,比如源位置,字節(jié)碼,嚴(yán)格或一般模式。除此之外,還有一個(gè)指向上下文的指針,其中包含自由變量的值以及對(duì)全局對(duì)象的訪問(wèn)。
關(guān)于自由變量和約束變量的概念, 閉包 (計(jì)算機(jī)科學(xué))
反饋向量的大致結(jié)構(gòu)如下,slot是一個(gè)槽,表示向量表里面的一項(xiàng),包含了操作類(lèi)型和傳入的值類(lèi)型,
IC Slot | IC Type | Value |
1 | Call | UNINIT |
2 | BinaryOp | SignedSmall |
比如,第二個(gè)是一個(gè) BinaryOp 槽,二元操作符類(lèi)似“+,-”等能夠記錄迄今為止看到的輸入和輸出的反饋。先不用糾結(jié)它的含義,后面我們會(huì)具體介紹。
如果你想查看你的函數(shù)所對(duì)應(yīng)的反饋向量,可以在你的代碼中加上專(zhuān)門(mén)的內(nèi)部函數(shù) ??%DebugPrint?
?? ,并且在d8中加上命令 ??–allow-natives-syntax?
? 來(lái)檢查特定閉包的反饋向量的內(nèi)容。
源代碼:
在d8 使用這個(gè)命令 –allow-natives-syntax 運(yùn)行,我們看到 :
我們看到調(diào)用次數(shù)(Invocation Count)是1,因?yàn)槲覀冎徽{(diào)用了一次函數(shù)add。此時(shí)還沒(méi)有優(yōu)化代碼(根據(jù)Optimized Code的值為0)。反饋向量的長(zhǎng)度為1,說(shuō)明里面只有一個(gè)槽,就是我們上面說(shuō)到的二元操作符槽(BinaryOp Slot),當(dāng)前反饋為 SignedSmall。
這個(gè)反饋SignedSmall代表什么?這表明指令A(yù)dd只看到了SignedSmall類(lèi)型的輸入,并且直到現(xiàn)在也只產(chǎn)生了SignedSmall類(lèi)型的輸出。
但是什么是SignedSmall類(lèi)型?JavaScript里面并不存在這種類(lèi)型。實(shí)際上,SignedSmall來(lái)自己V8中的一種優(yōu)化策略,它表示在程序中經(jīng)常使用的小的有符號(hào)整數(shù)(V8將高位的32位表示整數(shù),低位的全部置0來(lái)表示SignedSmall),這種類(lèi)型能夠獲得特殊處理(其他JavaScript引擎也有類(lèi)似的優(yōu)化策略)。
「值的表示」
V8通常使用一種叫做指針標(biāo)記(Pointer Tagging)的技術(shù)來(lái)表示值,應(yīng)用這種技術(shù),V8在每個(gè)值里面都設(shè)置一個(gè)標(biāo)識(shí)。我們處理的大部分值都分配在JavaScript堆上,并且由垃圾回收器(GC)來(lái)管理。但是對(duì)某些值來(lái)說(shuō),總是將它們分配在內(nèi)存里開(kāi)銷(xiāo)會(huì)很大。尤其是對(duì)于小整數(shù),它們通常會(huì)用在數(shù)組索引和一些臨時(shí)計(jì)算結(jié)果。
在V8中存在兩種指針標(biāo)識(shí)類(lèi)型:分別是是Smi(即 Small Integer的縮寫(xiě))和堆對(duì)象( HeapObject,就是JavaScript的引用類(lèi)型),其中堆對(duì)象是分配在內(nèi)存的堆中,圖中的地址即指向堆中的某塊地方。
我們用最低有效位來(lái)區(qū)分堆對(duì)象(標(biāo)志是1)和小整數(shù)(標(biāo)志是0)。對(duì)于64位結(jié)構(gòu)上的Smi,至少有32位有效位(低半部)是一直被置為0。另外32位,也就是Word的上半部,是被用來(lái)儲(chǔ)存32位有符號(hào)小整數(shù)的值。
僅僅是一次的執(zhí)行,還不足以讓引擎這么快下定決心,相信add 函數(shù)隨后的執(zhí)行都是Smi 類(lèi)型。那么我們先來(lái)看看,如果在隨后的執(zhí)行中,我們傳入不一樣的類(lèi)型會(huì)怎么樣。
反饋向量的變化
反饋類(lèi)型SignedSmall是指所有能用小整數(shù)表示的值。對(duì)于add操作而言,這意味著目前為止它只能看到輸入類(lèi)型為Smi,并且所產(chǎn)生的輸出值也都是Smi(也就是說(shuō),所有的值都沒(méi)有超過(guò)32位整數(shù)的范圍)。下面我們來(lái)看看,當(dāng)我們調(diào)用add的時(shí)候傳入一個(gè)不是Smi的值會(huì)發(fā)生什么。
在d8加入命令 –allow-natives-syntax ,然后看到下面結(jié)果。
首先,我們看到調(diào)用次數(shù)現(xiàn)在是2,因?yàn)檫\(yùn)行了兩次函數(shù)add。然后發(fā)現(xiàn)BinaryOp 槽的值現(xiàn)在變成了Number,這表明對(duì)于這個(gè)加法已經(jīng)有傳入了任意類(lèi)型的數(shù)值(即非整數(shù))。此外,這有一個(gè)反饋向量的狀態(tài)遷移圖,大致如下所示:
反饋狀態(tài)從 None 開(kāi)始,這表明目前還沒(méi)有看到任何輸入,所以什么都不知道。狀態(tài)Any表明我們看到了不兼容的(比如number和string)輸入和輸出的組合。狀態(tài)Any意味著Add(字節(jié)碼中的)是多態(tài)。相比之下,其余的狀態(tài)表明Add都是單態(tài)(monomorphic),因?yàn)榭吹降妮斎牒彤a(chǎn)生的都是相同類(lèi)型的值。下面是圖中名詞解釋?zhuān)?/p>
- SignedSmall 表示所有的值都是小整數(shù)(有效數(shù)值為是32位或者31位,取決于Word的在不同架構(gòu)上的大小),均表示為Smi。
- Number 表明所有的值都常規(guī)數(shù)字 (這包括小整數(shù))。
- NumberOrOddball 包括其他能被轉(zhuǎn)換成 Number 的 undefined, null, true 和 false 。
- String :所有輸入值都是字符串
- BigInt 表示輸入都是大整數(shù)。
需要注意一點(diǎn),反饋只能在這個(gè)圖中前進(jìn)(從 None 到 Any),不能回退。如果真的那樣做,那么我們就會(huì)有陷入去優(yōu)化循環(huán)的風(fēng)險(xiǎn)。那樣情況下,優(yōu)化編譯器發(fā)現(xiàn)輸入值與之前得到反饋內(nèi)容不同,比如之前解釋器生成的反饋是 Number,但現(xiàn)在輸入值出現(xiàn)了 String,這時(shí)候已經(jīng)生成的反饋和優(yōu)化代碼就會(huì)失效,并回退到解釋器生成的字節(jié)碼版本。當(dāng)下一次函數(shù)再次變熱(hot,多次運(yùn)行),我們將再次優(yōu)化它,如果允許回退,這時(shí)候優(yōu)化編譯器會(huì)再次生成相同的代碼,這意味著會(huì)再次回到 Number 的情況。如果這樣無(wú)限制的回退去優(yōu)化,再優(yōu)化,編譯器將會(huì)忙于優(yōu)化和去優(yōu)化,而不是高速運(yùn)行 JavaScript 代碼。
優(yōu)化管道(The Optimization Pipeline)
現(xiàn)在我們知道了解釋器Ignition 是如何為函數(shù)add收集反饋,下面來(lái)看看優(yōu)化編譯器如何利用反饋生成最小的代碼,因?yàn)開(kāi)越小的機(jī)器指令代碼塊,意味著更快的速度_。為了觀察,我將使用一個(gè)特殊的內(nèi)部函數(shù)OptimizeFunctionOnNextCall()在特定的時(shí)間點(diǎn)觸發(fā)V8對(duì)函數(shù)的優(yōu)化。我們經(jīng)常使用這些內(nèi)部函數(shù)以非常特定的方式對(duì)引擎進(jìn)行測(cè)試。
在這里,給函數(shù)add傳遞兩個(gè)整數(shù)型值來(lái)明確call site “x + y”的反饋會(huì)被預(yù)熱為小整數(shù)(表示_這個(gè)call site全部傳遞的都是小整數(shù),對(duì)于優(yōu)化引擎來(lái)說(shuō)將來(lái)得到的輸入也會(huì)是小整數(shù)_),并且結(jié)果也是屬于小整數(shù)范圍。然后我們告訴V8應(yīng)該在下次調(diào)用函數(shù)add的時(shí)候去優(yōu)化它(用TurboFan ),最終再次調(diào)用add,觸發(fā)優(yōu)化編譯器運(yùn)行生成機(jī)器碼。
TurboFan 拿到之前為函數(shù)add生成的字節(jié)碼,并從函數(shù)add的反饋向量表里提取出相關(guān)的反饋。優(yōu)化編譯器將這些信息轉(zhuǎn)換成一個(gè)圖表示,再將這個(gè)圖表示傳遞給前端,優(yōu)化以及后端的各個(gè)階段(見(jiàn)上圖)。在本文不會(huì)詳細(xì)展開(kāi)這部分內(nèi)容,這是另一個(gè)系列的內(nèi)容了。我們要了解的是最終生成的機(jī)器碼,并看看優(yōu)化推測(cè)是如何工作的。你可以在d8中加上命令 –print-opt-code來(lái)查看由TurboFan 生成的優(yōu)化代碼。
這是由TurboFan 在x64架構(gòu)上生成的機(jī)器碼,這里省略了一些無(wú)關(guān)緊要的技術(shù)細(xì)節(jié)(,下面就來(lái)看看這些代碼做了什么。
第一段代碼檢查對(duì)象是否仍然有效(對(duì)象的形狀是否符合之前生成機(jī)器碼的那個(gè)),或者某些條件是否發(fā)生了改變,這就需要丟棄這個(gè)優(yōu)化代碼。這部分具體內(nèi)容可以參考 Juliana Franco 的 “Internship on Laziness“。一旦我們知道這段代碼仍然有效,就會(huì)建立一個(gè)棧幀并且檢查堆棧上是否有足夠的空間來(lái)執(zhí)行代碼。
然后從函數(shù)主體開(kāi)始。我們從棧中讀取參數(shù)x和y的值(相對(duì)于幀指針rbp,比如rbp+1這樣的地址,請(qǐng)參考棧幀概念),然后檢查兩個(gè)參數(shù)是否都是 Smi 類(lèi)型(因?yàn)楦鶕?jù)“+”得到的反饋,兩個(gè)輸入總是Smi)。這一步是通過(guò)測(cè)試最低有效位來(lái)完成。一旦確定了參數(shù)都是Smi,我們需要將它轉(zhuǎn)換成32位表示,這是通過(guò)將值右移32位來(lái)完成的。如果x或y不是Smi,則會(huì)立即終止執(zhí)行優(yōu)化代碼,接著負(fù)責(zé)去優(yōu)化的模塊就會(huì)恢復(fù)到之前解釋器生成的函數(shù)add的代碼(即字節(jié)碼)。
然后我們繼續(xù)執(zhí)行對(duì)輸入值的整數(shù)加法,這時(shí)需要明確地測(cè)試溢出,因?yàn)榧臃ǖ慕Y(jié)果可能超出32位整數(shù)的范圍,在這種情況下就要返回到解釋器版本,并在隨后將add的反饋類(lèi)型提升為Number(之前說(shuō)過(guò),反饋類(lèi)型的改變只能前進(jìn))。
最后我們通過(guò)將帶符號(hào)的32位值向上移動(dòng)32位,將結(jié)果轉(zhuǎn)換回Smi表示,并將結(jié)果返回存到累加器rax 。
我們現(xiàn)在可以看到生成的代碼是高度優(yōu)化的,并且適用于專(zhuān)門(mén)的反饋。它完全不去處理其他數(shù)字,字符串,大整數(shù)或任意JavaScript對(duì)象,只關(guān)注目前為止我們所看到的那種類(lèi)型的值。這是使許多JavaScript應(yīng)用程序達(dá)到最佳性能的關(guān)鍵因素。
為什么需要TypeScript?
在上面的介紹中,我們竭力避免了對(duì)JavaScript 對(duì)象的訪問(wèn),如果有對(duì)象加入,這將會(huì)變成一個(gè)很復(fù)雜的話題。但為了更好的展開(kāi)這個(gè)話題,我們還是需要提一下,關(guān)于對(duì)象的優(yōu)化是V8中極其重要的一部分。例如,以下面這個(gè)對(duì)象為例
對(duì)于像 o.x這樣的屬性訪問(wèn),若o始終具有相同的形狀(形狀同結(jié)構(gòu),即相同的屬性以及屬性是相同的順序,例如o的結(jié)構(gòu)一直是{x:v},其中v的類(lèi)型是String),我們會(huì)把如何獲得o.x的過(guò)程信息緩存起來(lái),構(gòu)造成一個(gè)隱藏類(lèi)( Hidden Class)。在隨后執(zhí)行相同的字節(jié)碼時(shí),不需要再次搜索對(duì)象o中x的位置。這種底層實(shí)現(xiàn)被稱(chēng)為內(nèi)聯(lián)緩存– inline cache (IC)。
你可以在Vyacheslav Egoro寫(xiě)的這篇文章 “What’s up with monomorphism?” 中了解更多關(guān)于ICs和屬性訪問(wèn)的細(xì)節(jié)。
總而言之,你現(xiàn)在應(yīng)該了解到,作為一門(mén)弱類(lèi)型的語(yǔ)言,從最早的SELF和smalltalk 語(yǔ)言開(kāi)始,研究者就在不斷去優(yōu)化這種弱類(lèi)型語(yǔ)言的執(zhí)行效率。
「從執(zhí)行的角度來(lái)說(shuō),動(dòng)態(tài)類(lèi)型性能瓶頸很大程度是因?yàn)樗膭?dòng)態(tài)的類(lèi)型系統(tǒng),與靜態(tài)類(lèi)型的語(yǔ)言相比, JavaScript 程序需要額外的操作來(lái)處理類(lèi)型的動(dòng)態(tài)性,所以執(zhí)行效率比較低?!?/p>
說(shuō)了這么多,最關(guān)鍵的一點(diǎn)
「確定你的代碼將要看到的類(lèi)型很重要」
再加上另外一句話:
「作為動(dòng)態(tài)語(yǔ)言,你的程序可能在90%的時(shí)間里,都在處理和代碼邏輯無(wú)關(guān)的事情。即:確認(rèn)你的代碼是什么形狀」
從傳統(tǒng)的JavaScript 角度來(lái)說(shuō)。
你無(wú)法很好的保證 add 函數(shù)將要看到的類(lèi)型,哪怕你確實(shí)想要這么做。但在一個(gè)大型的系統(tǒng)中,維護(hù)每一個(gè)函數(shù)和對(duì)象的形狀,極其困難。
你可能在前99次都保證了add 看到的都是Smi 類(lèi)型,但是在第100次,add 看到了一個(gè)String,而在這之前,優(yōu)化編輯器,即TurboFan,已經(jīng)大膽的推測(cè)了你的函數(shù)只會(huì)看到Smi,那么這時(shí)候
Ops!
優(yōu)化編輯器將不得不認(rèn)為自己做了個(gè)錯(cuò)誤的預(yù)測(cè),它會(huì)立即把之前的優(yōu)化丟掉。從字節(jié)碼開(kāi)始重新執(zhí)行。
而如果你的代碼一直陷入優(yōu)化<->去優(yōu)化的怪圈,那么程序執(zhí)行將會(huì)變慢,慢到還不如不優(yōu)化。
大多數(shù)的瀏覽器都做了限制,當(dāng)優(yōu)化/去優(yōu)化循環(huán)發(fā)生的時(shí)候會(huì)嘗試跳出這種循環(huán)。比如,如果 JIT 做了 10 次以上的優(yōu)化并且又丟棄的操作,那么就不繼續(xù)嘗試去優(yōu)化這段代碼。
所以,到這里你應(yīng)該明白了,有兩點(diǎn)準(zhǔn)則:
- 「確保你的代碼是什么形狀很重要」
但比第一條更重要的是:
- 「確保你的代碼固定在某個(gè)形狀上」
而編寫(xiě)TypeScript ,從工程和語(yǔ)言的層面上幫助你解決了這兩個(gè)準(zhǔn)則,你可以暢快的使用TypeScript,而無(wú)需擔(dān)心你是否不小心違背了上面兩條準(zhǔn)則。