偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

V8 引擎:基于類型推測的性能優(yōu)化原理

開發(fā) 前端
本文的會介紹一些關(guān)于V8內(nèi)基于推測的優(yōu)化的技術(shù),以此來告訴大家,為什么需要TypeScript。

介紹

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

我們將以一段函數(shù)的執(zhí)行未展開,從函數(shù)執(zhí)行的角度來看看,一段代碼如何被執(zhí)行,優(yōu)化,再最后,你會了解,為什么TypeScript更好。

看完本文后,你不需要記住文章中出現(xiàn)的繁雜的指令和代碼,只需要在你的腦海中存在一個印象,避免寫出糟糕的代碼,以及,盡量使用TypeScript。

如何執(zhí)行代碼?

作為介紹的第一部分,我們會用一段簡短的篇幅帶大家看看,你的代碼如何被執(zhí)行

圖片

當(dāng)然,如果用簡單的流程圖表示,你可以把上面的過程理解為這樣一個線性的執(zhí)行過程,當(dāng)然可能并不嚴(yán)謹(jǐn),稍后我們會繼續(xù)介紹。

圖片

下面讓我們從一段具體的代碼來看一下這個過程。

一段簡單的代碼?

function add(x, y) {
return x + y;
}
console.log(add(1, 2))

如果你在chrome的DevTools console中運(yùn)行這段代碼,你可以看到預(yù)期的輸出值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í)際的的函數(shù)體。在解析階段,不可能知道程序中名稱和變量的綁定關(guān)系,這主要是因?yàn)椤坝腥さ淖兞柯暶魈嵘?guī)則”以及JavaScript中的eval,此外還有其他原因。

一旦我們構(gòu)建完成了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é)碼,我們首先需要從較高的層面來認(rèn)知解釋器如何工作。V8的解釋器是基于寄存器架構(gòu)(register machine)的(相對的是基于棧架構(gòu),也是早期V8版本中使用的 FullCodegen 編譯器)。Ignition 會把指令序列都保存在解釋器自身的(虛擬)寄存器中,這些寄存器部分被映射到實(shí)際CPU的寄存器中,而另外一部分會用實(shí)際機(jī)器的棧內(nèi)存來模擬。

圖片

有兩個特殊寄存器a0和a1對應(yīng)著函數(shù)在機(jī)器棧(即內(nèi)存棧)上的形式參數(shù)(在函數(shù)add這個例子中,有兩個形參)。形參是在源代碼中聲名的參數(shù),它可能與在運(yùn)行時傳遞給函數(shù)的實(shí)際參數(shù)數(shù)量不同。每個字節(jié)碼指令執(zhí)行得到的最終值通常被保存在一個稱作累加器(accumulator)的特殊寄存器中。堆棧指針(stack pointer )指向當(dāng)前的棧幀或者說激活記錄,程序計(jì)數(shù)器( program counter)指向指令字節(jié)碼中當(dāng)前正在執(zhí)行的指令。下面我們看看這個例子中每條字節(jié)碼指令都做了什么。

  • StackCheck 會將堆棧指針與一些已知的上限比較(實(shí)際上在V8中應(yīng)該稱作下限,因?yàn)闂J菑母叩刂返降偷刂废蛳律L的)。如果棧的增長超過了某個閾值,就會放棄函數(shù)的執(zhí)行,同時拋出一個 RangeError 來告訴我們棧溢出了。
  • Ldar a1將寄存器a1的值加載到累加器寄存器中(Ladr 表示 LoaD Accumulator Register)
  • Add a0, [0] 讀取寄存器a0里的值,并把它加到累加器的值上。結(jié)果被再次放到累加器中。

為什么這條指令是這樣的?以一條JS 語句為例

var dest = src1 + src2 // op dest, src1,src2
var dest += src; //op dest, src
+src; // op src

分別表示三地址指令,二地址指令,一地址指令,我在后面分別標(biāo)注了轉(zhuǎn)換后的機(jī)器指令。三地址和二地址指令都指定了運(yùn)算后儲存結(jié)果的位置。

但在一地址指令中,沒有指定目標(biāo)源。實(shí)際上,它會被默認(rèn)存在一個累加器”(accumulator)的專用寄存器,保存計(jì)算結(jié)果。

其中Add運(yùn)算符的[0]操作數(shù)指向一個「反饋向量槽( feedback vector slot)」,它是解釋器用來儲存有關(guān)函數(shù)執(zhí)行期間看到的值的分析信息。在后面講解TurboFan 如何優(yōu)化函數(shù)的時候會再次回到這。

  • Return 結(jié)束當(dāng)前函數(shù)的執(zhí)行,并把控制權(quán)交給調(diào)用者。返回值是累加器中的當(dāng)前值。

當(dāng)最終生成了上面這段字節(jié)碼后,會被送入的VM ,一般會由解釋器進(jìn)行執(zhí)行,這種執(zhí)行方式是最原始也是效率最低的。我們可以在下一部分了解到,這種原始的執(zhí)行會經(jīng)歷什么。

關(guān)于字節(jié)碼的解釋,這里不會做過多的贅述,如果你感興趣,可以擴(kuò)展閱讀 「Understanding V8’s Bytecode」 (https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775) 一文,這篇字節(jié)碼對V8的字節(jié)碼的工作原理提供了一些深入的了解。

為什么需要優(yōu)化?

現(xiàn)在,我相信你已經(jīng)對V8如何執(zhí)行一段代碼有了一個簡單的認(rèn)識。在正式進(jìn)入我們的主題之前,還需要解釋一個很關(guān)鍵的問題,為什么我們需要優(yōu)化。為了回答這個問題,我們需要先看下下規(guī)范

關(guān)于規(guī)范的更多了解,可以去這里查找 https://tc39.es/ecma262/#sec-toprimitive

圖片

圖片

我們再以看最常見的 ToPrimitive 為例,需要經(jīng)過非常繁瑣的求值過程,而這些過程都是為了解決操作類型的動態(tài)性

圖片

在JavaScript中的“+”運(yùn)算符已經(jīng)是一個相當(dāng)復(fù)雜的操作了,在最終執(zhí)行一個數(shù)值相加之前必須進(jìn)行大量的檢查

而如果引擎要想讓這些步驟是能夠在幾個機(jī)器指令內(nèi)完成以達(dá)到峰值性能(與C++相媲美),這里有一個關(guān)鍵能力—-推測優(yōu)化,通過假設(shè)可能的輸入。例如,當(dāng)我們知道表達(dá)式x+y中,x和y都是數(shù)字,那么我們就不需要處理任何一個是字符串或者其他更糟糕的情況—-操作數(shù)是任意類型的JavaScript對象,也就不需要對所有參數(shù)調(diào)用一遍 ToPrimitive 了。

換句話說,如果我們能夠確定x,y 都是數(shù)字類型,我們自然就很容易對這個函數(shù)執(zhí)行進(jìn)行優(yōu)化,消除冗余的IR指令。

「而從執(zhí)行的角度來說,動態(tài)類型性能瓶頸很大程度是因?yàn)樗膭討B(tài)的類型系統(tǒng),與靜態(tài)類型的語言相比,JavaScript 程序需要額外的操作來處理類型的動態(tài)性,所以執(zhí)行效率比較低?!?/strong>

那么如何確認(rèn)x,y都是數(shù)字,我們又如何優(yōu)化呢?

基于推測的優(yōu)化?

因?yàn)?JavaScript 動態(tài)語言的特性,我們通常直到運(yùn)行時才知道值的確切類型,僅僅觀察源代碼,往往不可能知道某個操作的可能輸入值。所以這就是為什么我們需要推測,根據(jù)之前運(yùn)行收集到的值的反饋,然后假設(shè)將來總會看到類似的值。這種方法聽起來可能作用相當(dāng)有限,但它已被證明適用于JavaScript這樣的動態(tài)語言。

你可能會聯(lián)想到CPU的分支預(yù)測能力,如果是這樣,那么恭喜你,你并沒有想錯。

我們再回到這段代碼

function add(x, y) {
return x + y;
}

你可能已經(jīng)想到了,作為一個動態(tài)類型的語言,推測的第一步,就是要收集到足夠多的信息,來預(yù)測 ??add?? 在今后的執(zhí)行中會遇到的類型。

所以,首先向你介紹反饋向量(Feedback Vector),它是我們執(zhí)行預(yù)測最核心的成員之一:負(fù)責(zé)儲存我們收集到的信息。

反饋向量(Feedback Vector)

當(dāng)一段代碼被初次執(zhí)行時,它所執(zhí)行的往往是解釋器產(chǎn)生的字節(jié)碼。當(dāng)這段字節(jié)碼每次的執(zhí)行后,都會會產(chǎn)生一些反饋信息,這些反饋信息會被儲存在「反饋向量」(過去叫類型反饋向量) 中,這個特殊的數(shù)據(jù)結(jié)構(gòu)會被鏈接在閉包上。如果從對象結(jié)構(gòu)的角度來看,反饋向量和其他相關(guān)的內(nèi)容會是這樣。

圖片

其中 SharedFunctionInfo,它包含了函數(shù)的一般信息,比如源位置,字節(jié)碼,嚴(yán)格或一般模式。除此之外,還有一個指向上下文的指針,其中包含自由變量的值以及對全局對象的訪問。

關(guān)于自由變量和約束變量的概念, 閉包 (計(jì)算機(jī)科學(xué))

反饋向量的大致結(jié)構(gòu)如下,slot是一個槽,表示向量表里面的一項(xiàng),包含了操作類型和傳入的值類型,

IC Slot

IC Type

Value

1

Call

UNINIT

2

BinaryOp

SignedSmall

比如,第二個是一個 BinaryOp 槽,二元操作符類似“+,-”等能夠記錄迄今為止看到的輸入和輸出的反饋。先不用糾結(jié)它的含義,后面我們會具體介紹。

如果你想查看你的函數(shù)所對應(yīng)的反饋向量,可以在你的代碼中加上專門的內(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 運(yùn)行,我們看到 :

$ out/Debug/d8 --allow-natives-syntax add.js
DebugPrint: 0xb5101ea9d89: [Function] in OldSpace
- feedback vector: 0xb5101eaa091: [FeedbackVector] in OldSpace
- length: 1
SharedFunctionInfo: 0xb5101ea99c9 <SharedFunctionInfo add>
Optimized Code: 0
Invocation Count: 1
Profiler Ticks: 0
Slot #0 BinaryOp BinaryOp:SignedSmall

我們看到調(diào)用次數(shù)(Invocation Count)是1,因?yàn)槲覀冎徽{(diào)用了一次函數(shù)add。此時還沒有優(yōu)化代碼(根據(jù)Optimized Code的值為0)。反饋向量的長度為1,說明里面只有一個槽,就是我們上面說到的二元操作符槽(BinaryOp Slot),當(dāng)前反饋為 SignedSmall。

這個反饋SignedSmall代表什么?這表明指令A(yù)dd只看到了SignedSmall類型的輸入,并且直到現(xiàn)在也只產(chǎn)生了SignedSmall類型的輸出。

但是什么是SignedSmall類型?JavaScript里面并不存在這種類型。實(shí)際上,SignedSmall來自己V8中的一種優(yōu)化策略,它表示在程序中經(jīng)常使用的小的有符號整數(shù)(V8將高位的32位表示整數(shù),低位的全部置0來表示SignedSmall),這種類型能夠獲得特殊處理(其他JavaScript引擎也有類似的優(yōu)化策略)。

「值的表示」

V8通常使用一種叫做指針標(biāo)記(Pointer Tagging)的技術(shù)來表示值,應(yīng)用這種技術(shù),V8在每個值里面都設(shè)置一個標(biāo)識。我們處理的大部分值都分配在JavaScript堆上,并且由垃圾回收器(GC)來管理。但是對某些值來說,總是將它們分配在內(nèi)存里開銷會很大。尤其是對于小整數(shù),它們通常會用在數(shù)組索引和一些臨時計(jì)算結(jié)果。

圖片

在V8中存在兩種指針標(biāo)識類型:分別是是Smi(即 Small Integer的縮寫)和堆對象( HeapObject,就是JavaScript的引用類型),其中堆對象是分配在內(nèi)存的堆中,圖中的地址即指向堆中的某塊地方。

我們用最低有效位來區(qū)分堆對象(標(biāo)志是1)和小整數(shù)(標(biāo)志是0)。對于64位結(jié)構(gòu)上的Smi,至少有32位有效位(低半部)是一直被置為0。另外32位,也就是Word的上半部,是被用來儲存32位有符號小整數(shù)的值。

僅僅是一次的執(zhí)行,還不足以讓引擎這么快下定決心,相信add 函數(shù)隨后的執(zhí)行都是Smi 類型。那么我們先來看看,如果在隨后的執(zhí)行中,我們傳入不一樣的類型會怎么樣。

反饋向量的變化

反饋類型SignedSmall是指所有能用小整數(shù)表示的值。對于add操作而言,這意味著目前為止它只能看到輸入類型為Smi,并且所產(chǎn)生的輸出值也都是Smi(也就是說,所有的值都沒有超過32位整數(shù)的范圍)。下面我們來看看,當(dāng)我們調(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 ,然后看到下面結(jié)果。

$ out/Debug/d8 --allow-natives-syntax add.js
DebugPrint: 0xb5101ea9d89: [Function] in OldSpace

- feedback vector: 0x3fd6ea9ef9: [FeedbackVector] in OldSpace
- length: 1
SharedFunctionInfo: 0x3fd6ea9989 <SharedFunctionInfo add>
Optimized Code: 0
Invocation Count: 2
Profiler Ticks: 0
Slot #0 BinaryOp BinaryOp:Number

首先,我們看到調(diào)用次數(shù)現(xiàn)在是2,因?yàn)檫\(yùn)行了兩次函數(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),因?yàn)榭吹降妮斎牒彤a(chǎn)生的都是相同類型的值。下面是圖中名詞解釋:

  • 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),反饋只能在這個圖中前進(jìn)(從 None 到 Any),不能回退。如果真的那樣做,那么我們就會有陷入去優(yōu)化循環(huán)的風(fēng)險。那樣情況下,優(yōu)化編譯器發(fā)現(xiàn)輸入值與之前得到反饋內(nèi)容不同,比如之前解釋器生成的反饋是 Number,但現(xiàn)在輸入值出現(xiàn)了 String,這時候已經(jīng)生成的反饋和優(yōu)化代碼就會失效,并回退到解釋器生成的字節(jié)碼版本。當(dāng)下一次函數(shù)再次變熱(hot,多次運(yùn)行),我們將再次優(yōu)化它,如果允許回退,這時候優(yōu)化編譯器會再次生成相同的代碼,這意味著會再次回到 Number 的情況。如果這樣無限制的回退去優(yōu)化,再優(yōu)化,編譯器將會忙于優(yōu)化和去優(yōu)化,而不是高速運(yùn)行 JavaScript 代碼。

優(yōu)化管道(The Optimization Pipeline)

現(xiàn)在我們知道了解釋器Ignition 是如何為函數(shù)add收集反饋,下面來看看優(yōu)化編譯器如何利用反饋生成最小的代碼,因?yàn)開越小的機(jī)器指令代碼塊,意味著更快的速度_。為了觀察,我將使用一個特殊的內(nèi)部函數(shù)OptimizeFunctionOnNextCall()在特定的時間點(diǎn)觸發(fā)V8對函數(shù)的優(yōu)化。我們經(jīng)常使用這些內(nèi)部函數(shù)以非常特定的方式對引擎進(jìn)行測試。

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”的反饋會被預(yù)熱為小整數(shù)(表示_這個call site全部傳遞的都是小整數(shù),對于優(yōu)化引擎來說將來得到的輸入也會是小整數(shù)_),并且結(jié)果也是屬于小整數(shù)范圍。然后我們告訴V8應(yīng)該在下次調(diào)用函數(shù)add的時候去優(yōu)化它(用TurboFan ),最終再次調(diào)用add,觸發(fā)優(yōu)化編譯器運(yùn)行生成機(jī)器碼。

圖片

TurboFan 拿到之前為函數(shù)add生成的字節(jié)碼,并從函數(shù)add的反饋向量表里提取出相關(guān)的反饋。優(yōu)化編譯器將這些信息轉(zhuǎn)換成一個圖表示,再將這個圖表示傳遞給前端,優(yōu)化以及后端的各個階段(見上圖)。在本文不會詳細(xì)展開這部分內(nèi)容,這是另一個系列的內(nèi)容了。我們要了解的是最終生成的機(jī)器碼,并看看優(yōu)化推測是如何工作的。你可以在d8中加上命令 –print-opt-code來查看由TurboFan 生成的優(yōu)化代碼。

圖片

這是由TurboFan 在x64架構(gòu)上生成的機(jī)器碼,這里省略了一些無關(guān)緊要的技術(shù)細(xì)節(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

第一段代碼檢查對象是否仍然有效(對象的形狀是否符合之前生成機(jī)器碼的那個),或者某些條件是否發(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 類型(因?yàn)楦鶕?jù)“+”得到的反饋,兩個輸入總是Smi)。這一步是通過測試最低有效位來完成。一旦確定了參數(shù)都是Smi,我們需要將它轉(zhuǎn)換成32位表示,這是通過將值右移32位來完成的。如果x或y不是Smi,則會立即終止執(zhí)行優(yōu)化代碼,接著負(fù)責(zé)去優(yōu)化的模塊就會恢復(fù)到之前解釋器生成的函數(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ù)加法,這時需要明確地測試溢出,因?yàn)榧臃ǖ慕Y(jié)果可能超出32位整數(shù)的范圍,在這種情況下就要返回到解釋器版本,并在隨后將add的反饋類型提升為Number(之前說過,反饋類型的改變只能前進(jìn))。

最后我們通過將帶符號的32位值向上移動32位,將結(jié)果轉(zhuǎn)換回Smi表示,并將結(jié)果返回存到累加器rax 。

我們現(xiàn)在可以看到生成的代碼是高度優(yōu)化的,并且適用于專門的反饋。它完全不去處理其他數(shù)字,字符串,大整數(shù)或任意JavaScript對象,只關(guān)注目前為止我們所看到的那種類型的值。這是使許多JavaScript應(yīng)用程序達(dá)到最佳性能的關(guān)鍵因素。

為什么需要TypeScript?

在上面的介紹中,我們竭力避免了對JavaScript 對象的訪問,如果有對象加入,這將會變成一個很復(fù)雜的話題。但為了更好的展開這個話題,我們還是需要提一下,關(guān)于對象的優(yōu)化是V8中極其重要的一部分。例如,以下面這個對象為例

var o = {
x: ''
}
var o1 = {
x: ''
y
}
//o1. o2

對于像 o.x這樣的屬性訪問,若o始終具有相同的形狀(形狀同結(jié)構(gòu),即相同的屬性以及屬性是相同的順序,例如o的結(jié)構(gòu)一直是{x:v},其中v的類型是String),我們會把如何獲得o.x的過程信息緩存起來,構(gòu)造成一個隱藏類( Hidden Class)。在隨后執(zhí)行相同的字節(jié)碼時,不需要再次搜索對象o中x的位置。這種底層實(shí)現(xiàn)被稱為內(nèi)聯(lián)緩存– inline cache (IC)。

你可以在Vyacheslav Egoro寫的這篇文章 “What’s up with monomorphism?” 中了解更多關(guān)于ICs和屬性訪問的細(xì)節(jié)。

總而言之,你現(xiàn)在應(yīng)該了解到,作為一門弱類型的語言,從最早的SELF和smalltalk 語言開始,研究者就在不斷去優(yōu)化這種弱類型語言的執(zhí)行效率。

「從執(zhí)行的角度來說,動態(tài)類型性能瓶頸很大程度是因?yàn)樗膭討B(tài)的類型系統(tǒng),與靜態(tài)類型的語言相比, JavaScript 程序需要額外的操作來處理類型的動態(tài)性,所以執(zhí)行效率比較低?!?/p>

說了這么多,最關(guān)鍵的一點(diǎn)

「確定你的代碼將要看到的類型很重要」

再加上另外一句話:

「作為動態(tài)語言,你的程序可能在90%的時間里,都在處理和代碼邏輯無關(guān)的事情。即:確認(rèn)你的代碼是什么形狀」

從傳統(tǒng)的JavaScript 角度來說。

function add(x, y) {
return x + y;
}

你無法很好的保證 add 函數(shù)將要看到的類型,哪怕你確實(shí)想要這么做。但在一個大型的系統(tǒng)中,維護(hù)每一個函數(shù)和對象的形狀,極其困難。

你可能在前99次都保證了add 看到的都是Smi 類型,但是在第100次,add 看到了一個String,而在這之前,優(yōu)化編輯器,即TurboFan,已經(jīng)大膽的推測了你的函數(shù)只會看到Smi,那么這時候

Ops!

優(yōu)化編輯器將不得不認(rèn)為自己做了個錯誤的預(yù)測,它會立即把之前的優(yōu)化丟掉。從字節(jié)碼開始重新執(zhí)行。

而如果你的代碼一直陷入優(yōu)化<->去優(yōu)化的怪圈,那么程序執(zhí)行將會變慢,慢到還不如不優(yōu)化。

大多數(shù)的瀏覽器都做了限制,當(dāng)優(yōu)化/去優(yōu)化循環(huán)發(fā)生的時候會嘗試跳出這種循環(huán)。比如,如果 JIT 做了 10 次以上的優(yōu)化并且又丟棄的操作,那么就不繼續(xù)嘗試去優(yōu)化這段代碼。

圖片

所以,到這里你應(yīng)該明白了,有兩點(diǎn)準(zhǔn)則:

  1. 「確保你的代碼是什么形狀很重要」

但比第一條更重要的是:

  1. 「確保你的代碼固定在某個形狀上」

而編寫TypeScript ,從工程和語言的層面上幫助你解決了這兩個準(zhǔn)則,你可以暢快的使用TypeScript,而無需擔(dān)心你是否不小心違背了上面兩條準(zhǔn)則。

責(zé)任編輯:姜華 來源: Tecvan
相關(guān)推薦

2017-12-17 16:34:18

JavaScript代碼V8

2021-05-28 05:30:55

HandleV8代碼

2022-06-02 12:02:12

V8C++JavaScript

2022-04-29 08:00:51

V8垃圾回收

2020-10-25 08:22:28

V8 引擎JavaScript回調(diào)函數(shù)

2022-06-21 08:52:47

Node.js服務(wù)端JavaScript

2022-02-25 08:32:07

nodemon搭Node.jsJavascript

2023-10-10 10:23:50

JavaScriptV8

2009-08-21 10:09:02

Google ChroV8引擎linux系統(tǒng)

2020-09-27 07:32:18

V8

2009-07-20 09:36:04

谷歌瀏覽器安全漏洞

2022-11-04 07:12:24

JavaScript基準(zhǔn)測試

2010-07-20 16:35:52

V8JavaScript瀏覽器

2023-06-07 16:00:40

JavaScriptV8語言

2023-06-05 16:38:51

JavaScript編程語言V8

2022-09-16 08:32:25

JavaC++語言

2024-06-27 11:22:34

2014-11-26 09:51:24

GithubGoogleV8

2010-08-31 11:42:03

DB2MDC

2020-10-12 06:35:34

V8JavaScript
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號