JSConf EU 2018圓滿結束, 谷歌V8的開發者Mathias Bynens以及Benedikt Meurer一起發表了《JavaScript Engines: The Good Parts™》演講,本文將帶領大家回顧一下演講上所提到的重點。
演講第一部分: JavaScript引擎
JavaScript引擎
JavaScript引擎解析原始碼並將其轉換成抽象語法樹(AST)。基於AST,直譯器產生位元組碼。此時,引擎正在執行JavaScript程式碼。為了加快執行速度,位元組碼連同分析資料一起傳送到編譯器。編譯器根據已有的分析資料做出某些假設,然後生成優化後機器程式碼。

JavaScript引擎中的直譯器/編譯器
通過對比主流JavaScript引擎之間的一些實現差異來說明JavaScript引擎是如何執行你的程式碼。
直譯器快速生成未優化的位元組碼,編譯器會花費更長的時間,但最終產生高度優化的機器程式碼。





直譯器可以快速生成位元組碼,但位元組碼通常執行效率不高。另一方面,編譯器需要更長的時間,但最終會產生更高效的機器程式碼。快速獲取程式碼以執行(直譯器)或佔用更多時間,但最終以最佳效能執行程式碼(編譯器)之間存在權衡。
演講第二部分:JavaScript的物件模型
ECMAScript規範基本上將所有物件定義為字典,並將字串鍵對映到描述物件。

JavaScript對於陣列的定義類似於物件。例如,包括陣列索引在內的所有鍵都顯式表示為字串。陣列中的第一個元素儲存在鍵“0”。


演講第三部分:屬性的訪問優化
屬性訪問是JavaScript程式中最常見的操作。對JavaScript引擎來說,快速訪問屬性是至關重要的。
const object = {
foo: 'bar',
baz: 'qux',
};
// Here, we’re accessing the property `foo` on `object`:
doSomething(object.foo);
// ^^^^^^^^^^
複製程式碼
Shape
在JavaScript程式中,具有相同屬性鍵的物件是常見的。這樣的物件具有相同的Shape。
const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };
// `object1` and `object2` have the same shape.`
在相同Shape的物件上訪問相同的屬性也是非常常見的:
`function logX(object) {
console.log(object.x);
// ^^^^^^^^
}
const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };
logX(object1);
logX(object2);
複製程式碼
所以,JavaScript引擎可以基於物件的Shape優化屬性的訪問。
假設我們有一個屬性為x和y的物件,它使用我們前面討論過的字典資料結構:它包含作為字串的鍵,並且他們指向各自屬性的描述物件。

如果每個JS物件都儲存描述物件,會造成大量的重複和不必要的記憶體開銷。JavaScript引擎會將這些物件的Shape分開儲存。


所有JavaScript引擎都使用Shape作為優化,但它們並不都稱之為Shape:
- 學術論文稱之為Hidden Classes
- V8稱之為Maps
- Chakra稱之為Types
- JavaScriptCore稱之為Structures
- SpiderMonkey稱之為Shapes 演講中統一使用了Shape。
過渡鏈與過渡樹
如果一個物件指向某個Shape,你給它新增一個新的屬性,JavaScript引擎如何找到新的Shape。這類Shape在JavaScript引擎中形成所謂的“過渡鏈”。下面是一個例子:

我們甚至不需要為每個Shape儲存完整的屬性表。相反,每一個Shape僅需要知道它所引入的新屬性。例如,在這種情況下,我們不必在最後一個Shape中儲存關於“x”的資訊,因為它可以在鏈中更早地找到。為了做到這一點,每一個Shape都和上一個Shape產生連結:

但是如果沒有辦法建立一個過渡鏈怎麼辦?例如,如果有兩個空物件,並且向每個物件新增不同的屬性呢?
const object1 = {};
object1.x = 5;
const object2 = {};
object2.y = 6;
複製程式碼
在這種情況下,我們必須使用分支取代鏈,我們最終得到一個過渡樹:

引擎對已經包含屬性的物件應用了一些優化。要麼從空物件開始新增“x”,要麼有一個已經包含“x”的物件:
const object1 = {};
object1.x = 5;
const object2 = { x: 6 };
複製程式碼

內聯快取(ICs)
ICs是使JavaScript快速執行的關鍵因素!JavaScript引擎使用ICs來記住在何處查詢物件屬性的資訊,以減少查詢次數。 這裡有一個函式getX,它獲取一個物件並從中載入屬性“x”:
function getX(o) {
return o.x;
}
複製程式碼
如果我們在JSC中執行這個函式,它會生成下面的位元組碼:

JSC還將內聯快取嵌入到get_by_id指令中,該指令由兩個未初始化的槽組成。



演講第四部分:有效的儲存陣列
陣列使用陣列索引來儲存屬性。這些屬性的值稱為陣列元素。為每個陣列元素儲存描述物件是不明智的。陣列索引屬性預設為可寫、可列舉和可配置,JavaScript引擎將陣列元素與其他屬性分開儲存。
看一下這個陣列:
const array = [
'#jsconfeu',
];
複製程式碼
引擎儲存的陣列長度為1,並指向包含length的Shape,偏移值為0。


如果更改陣列元素的描述物件,會怎麼樣?
// Please don’t ever do this!
const array = Object.defineProperty(
[],
'0',
{
value: 'Oh noes!!1',
writable: false,
enumerable: false,
configurable: false,
}
);
複製程式碼
上面的程式碼段定義了一個名為“0”的屬性(恰好是一個陣列索引),但它將屬性設定為非預設值。
在這樣的極端情況下,JavaScript引擎將整個元素後備儲存區作為字典,對映描述物件到每個陣列索引。

結語
本次演講讓我們明白JavaScript引擎是如何工作的,如何儲存物件和陣列,以及如何通過Shape和ICs優化了屬性的訪問,如何優化了陣列的儲存。基於這些知識,為我們確定了一些實用的可以幫助提高效能的編碼技巧:
- 總是以同樣的方式初始化物件,它們最終會有相同的Shape。
- 不要修改陣列元素的描述物件,它們可以有效地儲存。
註記
- 本文結構及程式碼來自 Mathias Bynens以及Benedikt Meurer 在 JSConf EU 2018 上所作的演講 JavaScript Engines: The Good Parts™。錄影地址:https://www.youtube.com/watch?v=5nmpokoRaZI&index=11&list=PL37ZVnwpeshG2YXJkun_lyNTtM-Qb3MKa
- 同時也可以閱讀本次演講的Blog:https://mathiasbynens.be/notes/shapes-ics