JavaScript 引擎基礎(chǔ):原型優(yōu)化
本文就所有 JavaScript 引擎中常見的一些關(guān)鍵基礎(chǔ)內(nèi)容進(jìn)行了介紹——這不僅僅局限于 V8 引擎。作為一名 JavaScript 開發(fā)者,深入了解 JavaScript 引擎是如何工作的將有助于你了解自己所寫代碼的性能特征。
在前一篇文章中,我們討論了 JavaScript 引擎是如何通過 Shapes 和 Inline Caches 來優(yōu)化對象與數(shù)組的訪問的。本文將介紹優(yōu)化流程中的權(quán)衡與取舍,并對引擎在優(yōu)化原型屬性訪問方面的工作進(jìn)行介紹。
原文 JavaScript engine fundamentals: optimizing prototypes ,作者 @Benedikt 和 @Mathias ,譯者 hijiangtao 。以下開始正文。
如果你傾向看視頻演講,請移步 YouTube 查看更多。
1. 優(yōu)化層級與執(zhí)行效率的取舍前一篇文章介紹了現(xiàn)代 JavaScript 引擎通用的處理流程:
我們也指出,盡管從高級抽象層面來看,引擎之間的處理流程都很相似,但他們在優(yōu)化流程上通常都存在差異。為什么呢? 為什么有些引擎的優(yōu)化層級會比其他引擎更多? 事實證明,在快速獲取可運行的代碼與花費更多時間獲得最優(yōu)運行性能的代碼之間存在取舍與平衡。
解釋器可以快速生成字節(jié)碼,但字節(jié)碼通常效率不高。 相比之下,優(yōu)化編譯器雖然需要更長的時間,但最終會產(chǎn)生更高效的機器碼。
這正是 V8 在使用的模型。V8 的解釋器叫 Ignition,(就原始字節(jié)碼執(zhí)行速度而言)它是所有引擎中最快的解釋器。V8 的優(yōu)化編譯器名為 TurboFan,它最終會生成高度優(yōu)化的機器碼。
啟動延遲與執(zhí)行速度之間的這些權(quán)衡便是一些 JavaScript 引擎決定是否在流程中加入優(yōu)化層的原因。例如,SpiderMonkey 在解釋器和完整的 IonMonkey 優(yōu)化編譯器之間添加了一個 Baseline 層:
解釋器可以快速生成字節(jié)碼,但字節(jié)碼執(zhí)行起來相對較慢。Baseline 生成代碼需要花費更長的時間,但能提供更好的運行時性能。最后,IonMonkey 優(yōu)化編譯器花費最長時間來生成機器碼,但該代碼運行起來非常高效。
讓我們通過一個具體的例子,看看不同引擎中的優(yōu)化流程都有哪些差異。這是一些在循環(huán)中會經(jīng)常重復(fù)的代碼。
let result = 0;for (let i = 0; i < 4242424242; ++i) {result += i;}console.log(result);
V8開始在 Ignition 解釋器中運行字節(jié)碼。從某些方面來看,代碼是否足夠 hot 由引擎決定,引擎話負(fù)責(zé)調(diào)度 TurboFan 前端,它是 TurboFan 中負(fù)責(zé)處理集成分析數(shù)據(jù)和構(gòu)建代碼在機器層面表示的一部分。這部分結(jié)果之后會被發(fā)送到另一個線程上的 TurboFan 優(yōu)化器被進(jìn)一步優(yōu)化。
當(dāng)優(yōu)化器運行時,V8 會繼續(xù)在 Ignition 中執(zhí)行字節(jié)碼。 當(dāng)優(yōu)化器處理完成后,我們獲得可執(zhí)行的機器碼,執(zhí)行流程便會繼續(xù)下去。
SpiderMonkey 引擎也開始在解釋器中運行字節(jié)碼。但它有一個額外的 Baseline 層,這意味著比較 hot 的代碼會首先被發(fā)送到 Baseline。 Baseline 編譯器在主線程上生成 Baseline 代碼,并在完成后繼續(xù)后面的執(zhí)行。
如果 Baseline 代碼運行了一段時間,SpiderMonkey 最終會激活 IonMonkey 前端,并啟動優(yōu)化器 - 這與 V8 非常相似。當(dāng) IonMonkey 進(jìn)行優(yōu)化時,代碼在 Baseline 中會一直運行。當(dāng)優(yōu)化器處理完成后,被執(zhí)行的是優(yōu)化后的代碼而不是 Baseline 代碼。
Chakra 的架構(gòu)與 SpiderMonkey 非常相似,但 Chakra 嘗試并行處理更多內(nèi)容以避免阻塞主線程。Chakra 不在主線程上運行編譯器,而是將不同編譯器可能需要的字節(jié)碼和分析數(shù)據(jù)復(fù)制出來,并將其發(fā)送到一個專用的編譯器進(jìn)程。
當(dāng)代碼準(zhǔn)備就緒,引擎便開始運行 SimpleJIT 代碼而不是字節(jié)碼。 對于 FullJIT 來說流程也是同樣如此。這種方法的好處是,與運行完整的編譯器(前端)相比,復(fù)制所產(chǎn)生的暫停時間通常要短得多。但這種方法的缺點是這種 啟發(fā)式復(fù)制 可能會遺漏某些優(yōu)化所需的某些信息,因此它在一定程度上是用代碼質(zhì)量來換時間的消耗。
在 JavaScriptCore 中,所有優(yōu)化編譯器都與主 JavaScript 執(zhí)行 完全并發(fā)運行 ;根本沒有復(fù)制階段!相反,主線程僅僅是觸發(fā)了另一個線程上的編譯作業(yè)。然后,編譯器使用復(fù)雜的加鎖方式從主線程中獲取到要訪問的分析數(shù)據(jù)。
這種方法的優(yōu)點在于它減少了主線程上由 JavaScript 優(yōu)化引起的抖動。 缺點是它需要處理復(fù)雜的多線程問題并為各種操作付出一些加鎖的成本。
我們已經(jīng)討論過在使用解釋器快速生成代碼或使用優(yōu)化編譯器生成可高效執(zhí)行代碼之間的一些權(quán)衡。但還有另一個權(quán)衡: 內(nèi)存使用 !為了說明這一點,來看一個兩個數(shù)字相加的簡單 JvaScript 程序。
function add(x, y) {return x + y;}add(1, 2);
這是我們使用 V8 中的 Ignition 解釋器為 add 函數(shù)生成的字節(jié)碼:
StackCheckLdar a1Add a0, [0]Return
不要在意這些字節(jié)碼 - 你真的不需要閱讀它。關(guān)鍵是它只是 四條指令!
當(dāng)代碼變得 hot,TurboFan 便會生成以下高度優(yōu)化的機器碼:
leaq rcx,[rip+0x0]movq rcx,[rcx-0x37]testb [rcx+0xf],0x1jnz CompileLazyDeoptimizedCodepush rbpmovq rbp,rsppush rsipush rdicmpq rsp,[r13+0xe88]jna StackOverflowmovq rax,[rbp+0x18]test al,0x1jnz Deoptimizemovq rbx,[rbp+0x10]testb rbx,0x1jnz Deoptimizemovq rdx,rbxshrq rdx, 32movq rcx,raxshrq rcx, 32addl rdx,rcxjo Deoptimizeshlq rdx, 32movq rax,rdxmovq rsp,rbppop rbpret 0x18
這么 一大堆 代碼,尤其是與四條字節(jié)碼相比!通常,字節(jié)碼比機器碼更緊湊,特別是優(yōu)化過的機器碼。但另一方面,字節(jié)碼需要解釋器才能執(zhí)行,而優(yōu)化過機器碼可以由處理器直接執(zhí)行。
這就是為什么 JavaScript 引擎不簡單粗暴”優(yōu)化一切”的主要原因之一。正如我們之前所見,生成優(yōu)化的機器碼也需要很長時間,而最重要的是,我們剛剛了解到優(yōu)化的機器碼也需要更多的內(nèi)存。
小結(jié):JavaScript 引擎之所以具有不同優(yōu)化層,就在于使用解釋器快速生成代碼或使用優(yōu)化編譯器生成高效代碼之間存在一個基本權(quán)衡。通過添加更多優(yōu)化層可以讓你做出更細(xì)粒度的決策,但是以額外的復(fù)雜性和開銷為代價。此外,在優(yōu)化級別和生成代碼所占用的內(nèi)存之間也存在折衷。這就是為什么 JavaScript 引擎僅嘗試優(yōu)化比較 hot 功能的原因所在。
2. 原型屬性訪問優(yōu)化之前的文章解釋了 JavaScript 引擎如何使用 Shapes 和 Inline Caches 優(yōu)化對象屬性加載。回顧一下,引擎將對象的 Shape 與對象值分開存儲。
Shapes 可以實現(xiàn)稱為 Inline Caches 或簡稱 ICs 的優(yōu)化。通過組合,Shapes 和 ICs 可以加快代碼中相同位置的重復(fù)屬性訪問速度。
既然我們知道如何在 JavaScript 對象上快速進(jìn)行屬性訪問,那么讓我們看一下最近添加到 JavaScript 中的特性:class。JavaScript 中 class 的語法如下所示:
class Bar {constructor(x) {this.x = x;}getX() {return this.x;}}
盡管這看上去是 JavaScript 中的一個全新概念,但它僅僅是基于原型編程的語法糖:
function Bar(x) {this.x = x;}Bar.prototype.getX = function getX() {return this.x;};
在這里,我們在 Bar.prototype 對象上分配一個 getX 屬性。這與其他任何對象的工作方式完全相同,因為原型只是 JavaScript 中的對象!在基于原型的編程語言(如 JavaScript)中,方法通過原型共享,而字段則存儲在實際的實例上。
讓我們來實際看看,當(dāng)我們創(chuàng)建一個名為 foo 的 Bar 新實例時,幕后所發(fā)生的事情。
const foo = new Bar(true);
通過運行此代碼創(chuàng)建的實例具有一個帶有屬性 “x” 的 shape。 foo 的原型是屬于 class Bar 的 Bar.prototype 。
Bar.prototype 有自己的 shape,其中包含一個屬性 ’getX’ ,取值則是函數(shù) getX ,它在調(diào)用時只返回 this.x 。 Bar.prototype 的原型是 Object.prototype ,它是 JavaScript 語言的一部分。由于 Object.prototype 是原型樹的根節(jié)點,因此它的原型是 null 。
如果你在這個類上創(chuàng)建另一個實例,那么兩個實例將共享對象 shape。兩個實例都指向相同的 Bar.prototype 對象。
2.2 原型屬性訪問好的,現(xiàn)在我們知道當(dāng)我們定義一個類并創(chuàng)建一個新實例時會發(fā)生什么。但是如果我們在一個實例上調(diào)用一個方法會發(fā)生什么,比如我們在這里做了什么?
class Bar {constructor(x) { this.x = x; }getX() { return this.x; }}const foo = new Bar(true);const x = foo.getX();//^^^^^^^^^^
你可以將任何方法調(diào)用視為兩個單獨的步驟:
const x = foo.getX();// is actually two steps:const $getX = foo.getX;const x = $getX.call(foo);
第1步是加載這個方法,它只是原型上的一個屬性(其值恰好是一個函數(shù))。第2步是使用實例作為 this 值來調(diào)用該函數(shù)。讓我們來看看第一步,即從實例 foo 中加載方法 getX 。
引擎從 foo 實例開始,并且意識到 foo 的 shape 上沒有 ’getX’ 屬性,所以它必須向原型鏈追溯。我們到了 Bar.prototype ,查看它的原型 shape,發(fā)現(xiàn)它在偏移0處有 ’getX’ 屬性。我們在 Bar.prototype 的這個偏移處查找該值,并找到我們想要的 JSFunction getX 。就是這樣!
但 JavaScript 的靈活性使得我們可以改變原型鏈鏈接,例如:
const foo = new Bar(true);foo.getX();// → trueObject.setPrototypeOf(foo, null);foo.getX();// → Uncaught TypeError: foo.getX is not a function
在這個例子中,我們調(diào)用 foo.getX() 兩次,但每次它都具有完全不同的含義和結(jié)果。 這就是為什么盡管原型只是 JavaScript 中的對象,但優(yōu)化原型屬性訪問對于 JavaScript 引擎而言比優(yōu)化常規(guī)對象的屬性訪問更具挑戰(zhàn)性的原因了。
粗略的來看,加載原型屬性是一個非常頻繁的操作:每次調(diào)用一個方法時都會發(fā)生這種情況!
class Bar {constructor(x) { this.x = x; }getX() { return this.x; }}const foo = new Bar(true);const x = foo.getX();//^^^^^^^^^^
之前,我們討論了引擎如何通過使用 Shapes 和 Inline Caches 來優(yōu)化訪問常規(guī)屬性的。 我們?nèi)绾卧诰哂邢嗨?shape 的對象上優(yōu)化原型屬性的重復(fù)訪問呢? 我們在上面已經(jīng)看過是如何訪問屬性的。
為了在這種特殊情況下實現(xiàn)快速重復(fù)訪問,我們需要知道這三件事:
foo 的 shape 不包含 ’getX’ 并且沒有改變過。這意味著沒有人通過添加或刪除屬性或通過更改其中一個屬性來更改對象 foo 。 foo 的原型仍然是最初的 Bar.prototype 。這意味著沒有人通過使用 Object.setPrototypeOf() 或通過賦予特殊的 _proto_ 屬性來更改 foo 的原型。 Bar.prototype 的形狀包含 ’getX’ 并且沒有改變。這意味著沒有人通過添加或刪除屬性或更改其中一個屬性來更改 Bar.prototype 。一般情況下,這意味著我們必須對實例本身執(zhí)行1次檢查,并對每個原型進(jìn)行2次檢查,直到找到我們正在尋找的屬性所在原型。 1 + 2N 次檢查(其中 N 是所涉及的原型的數(shù)量)對于這種情況聽起來可能不太糟糕,因為這里原型鏈相對較淺 - 但是引擎通常必須處理更長的原型鏈,就像常見的 DOM 類一樣。這是一個例子:
const anchor = document.createElement(’a’);// → HTMLAnchorElementconst title = anchor.getAttribute(’title’);
我們有一個 HTMLAnchorElement ,在其上調(diào)用 getAttribute() 方法。這個簡單的錨元素原型鏈就已經(jīng)涉及6個原型!大多數(shù)有趣的 DOM 方法并不是直接存在于 HTMLAnchorElement 原型中,而是在原型鏈的更高層。
我們可以在 Element.prototype 上找到 getAttribute() 方法。這意味著我們每次調(diào)用 anchor.getAttribute() 時,JavaScript引擎都需要……
’getAttribute’HTMLAnchorElement.prototypeHTMLElement.prototype’getAttribute’Element.prototype’getAttribute’
總共有7次檢測!由于這是 Web 上一種非常常見的代碼,因此引擎會應(yīng)用技巧來減少原型上屬性加載所需的檢查次數(shù)。
回到前面的例子,我們在 foo 上訪問 ’getX’ 時總共執(zhí)行了3次檢查:
class Bar {constructor(x) { this.x = x; }getX() { return this.x; }}const foo = new Bar(true);const $getX = foo.getX;
在直到我們找到攜帶目標(biāo)屬性的原型之前,我們需要對原型鏈上的每個對象進(jìn)行 shape 的缺失檢查。如果我們可以通過將原型檢查折疊到缺失檢查來減少檢查次數(shù),那就太好了。而這基本上就是引擎所做的: 引擎將原型鏈在 Shape 上,而不是直接鏈在實例上。
每個 shape 都指向原型。這也意味著每次 foo 原型發(fā)生變化時,引擎都會轉(zhuǎn)換到一個新 shape。 現(xiàn)在我們只需要檢查一個對象的 shape,這樣既可以斷言某些屬性的缺失,也可以保護(hù)原型鏈鏈接。
通過這種方法,我們可以將檢查次數(shù)從 1 + 2N 降到 1 + N ,以便在原型上更快地訪問屬性。但這仍相當(dāng)昂貴,因為它在原型鏈的長度上仍然是線性的。 為了進(jìn)一步將檢查次數(shù)減少到一個常量級別,引擎采用了不同的技巧,特別是對于相同屬性訪問的后續(xù)執(zhí)行。
2.3 Validity cellsV8專門為此目的處理原型的 shape。每個原型都具有一個不與其他對象(特別是不與其他原型共享)共享且獨特的 shape,且每個原型的 shape 都具有與之關(guān)聯(lián)的一個特殊 ValidityCell 。
只要有人更改相關(guān)原型或其祖先的任何原型,此 ValidityCell 就會失效。讓我們來看看它是如何工作的。
為了加速原型的后續(xù)訪問,V8 建立了一個 Inline Cache,其中包含四個字段:
在第一次運行此代碼預(yù)熱 inline cache 時,V8 會記住目標(biāo)屬性在原型中的偏移量,找到屬性的原型(本例中為 Bar.prototype ),實例的 shape(在這種情況下為 foo 的 shape),以及與實例 shape 鏈接的 直接原型 中 ValidityCell 的鏈接(在本例中也恰好是 Bar.prototype )。
下次 inline cache 命中時,引擎必須檢查實例的 shape 和 ValidityCell 。如果它仍然有效,則引擎可以直接到達(dá) Prototype 上的 Offset 位置,跳過其他查找。
當(dāng)原型改變時,shape 將重新分配,且先前的 ValidityCell 失效。因此,Inline Cache 在下次執(zhí)行時會失效,從而導(dǎo)致性能下降。
回到之前的 DOM 示例,這意味著對 Object.prototype 的任何更改不僅會使 Object.prototype 本身的 inline cache 失效,而且還會使其下游的所有原型失效,包括 EventTarget.prototype , Node.prototype , Element.prototype 等,直到 HTMLAnchorElement.prototype 為止。
實際上,在運行代碼時修改 Object.prototype 意味著完全拋棄性能上的考慮。不要這樣做!
讓我們用一個具體的例子來探討這個問題。 假設(shè)我們有一個類叫做 Bar ,并且我們有一個函數(shù) loadX ,它調(diào)用 Bar 對象上的方法。 我們用同一個類的實例多調(diào)用這個 loadX 函數(shù)幾次。
class Bar { /* … */ }function loadX(bar) {return bar.getX(); // IC for ’getX’ on `Bar` instances.}loadX(new Bar(true));loadX(new Bar(false));// IC in `loadX` now links the `ValidityCell` for// `Bar.prototype`.Object.prototype.newMethod = y => y;// The `ValidityCell` in the `loadX` IC is invalid// now, because `Object.prototype` changed.
loadX 中的 inline cache 現(xiàn)在指向 Bar.prototype 的 ValidityCell 。 如果你之后執(zhí)行了類似于改變 Object.prototype (這是 JavaScript 中所有原型的根節(jié)點)的操作,則 ValidityCell 將失效,且現(xiàn)有的 inline cache 會在下次命中時丟失,從而導(dǎo)致性能下降。
修改 Object.prototype 被認(rèn)為是一個不好的操作,因為它使引擎在此之前為原型訪問準(zhǔn)備的所有 inline cache 都失效。 這是另一個 不推薦 的例子:
Object.prototype.foo = function() { /* … */ };// Run critical code:someObject.foo();// End of critical code.delete Object.prototype.foo;
我們擴展了 Object.prototype ,它使引擎在此之前存儲的所有原型 inline cache 均無效了。然后我們運行一些用到新原型方法的代碼。引擎此時則需要從頭開始,并為所有原型屬性的訪問設(shè)置新的 inline cache。最后,我們刪除了之前添加的原型方法。
清理,這聽起來像個好主意,對吧?然而在這種情況下,它只會讓情況變得更糟!刪除屬性會修改 Object.prototype ,因此所有 inline cache 會再次失效,而引擎又必須從頭開始。
總結(jié):雖然原型只是對象,但它們由 JavaScript 引擎專門處理,以優(yōu)化在原型上查找方法的性能表現(xiàn)。把你的原型放在一旁!或者,如果你確實需要修改原型,請在其他代碼運行之前執(zhí)行此操作,這樣至少不會讓引擎所做的優(yōu)化付諸東流。
5. Take-aways我們已經(jīng)了解了 JavaScript 引擎是如何存儲對象與類的, Shapes 、 Inline Caches 和 ValidityCells 是如何幫助優(yōu)化原型的。基于這些知識,我們認(rèn)為存在一個實用的 JavaScript 編碼技巧,可以幫助提高性能:不要隨意修改原型對象(即便你真的需要,那么請在其他代碼運行之前做這件事)。
(完)
來自:https://hijiangtao.github.io/2018/08/21/Prototypes/
相關(guān)文章:
1. Python sorted排序方法如何實現(xiàn)2. Python基于requests實現(xiàn)模擬上傳文件3. ASP.NET MVC實現(xiàn)橫向展示購物車4. windows服務(wù)器使用IIS時thinkphp搜索中文無效問題5. python利用opencv實現(xiàn)顏色檢測6. Python文本文件的合并操作方法代碼實例7. Python 中如何使用 virtualenv 管理虛擬環(huán)境8. 通過CSS數(shù)學(xué)函數(shù)實現(xiàn)動畫特效9. asp讀取xml文件和記數(shù)10. Python獲取B站粉絲數(shù)的示例代碼
