本文是根據本身的理解翻譯而來,若有疑惑可查看原文 JavaScript engine fundamentals: Shapes and Inline Caches。react
本次暫定翻譯三篇文章:編程
一切從你寫的 JavaScript 代碼開始。JavaScript 引擎會解析源碼並將其轉換成抽象語法樹(AST)。基於 AST,解釋器(interpreter)會進一步地生成字節碼。數組
爲了可以運行得更快,字節碼可能會和分析數據(profiling data)一同發給優化編譯器(the optimizing compiler)。優化編譯器會根據這些分析數據做出某些假設以今生成高度優化的機器碼。瀏覽器
若是某個時刻,某種假設被證實是錯誤的,優化編譯器將去優化並回滾到解釋器部分。緩存
如今來關注下 JavaScript 代碼被解釋和優化的地方,並重溫下主流 JavaScript 引擎之間的不一樣之處。數據結構
通常來講,在運行 JavaScript 代碼過程當中,會有解釋器和優化編譯器的參與。解釋器會快速地生成還沒有優化的字節碼,而優化編譯器會耗費一些時間來生成高度優化的機器碼。架構
上面的流程和 V8 在瀏覽器和 Node 環境下的工做流程及其類似:app
V8 引擎的解釋器被稱做 Ignition,主要負責生成和執行字節碼。當字節碼運行時,解釋器會收集分析數據,這些數據以後可能會被用來提高執行速度。若是一個函數常常被調用,即 hot,那麼,通過解釋器轉換來的字節碼和收集到的分析數據會傳給 TurboFan(V8 的優化編譯器),進一步被加工成高度優化的機器碼。ide
SpiderMonkey,Mozilla 的 JavaScript 引擎,擁有兩個優化編譯器,Baseline 和 IonMonkey。解釋器將轉換後的代碼傳給 Baseline 編譯器,Baseline 編譯器會將其加工成部分優化的代碼。再加上收集到的分析數據,IonMonkey 編譯器就能夠生成高度優化的代碼。若是基於假設的優化不成立,IonMonkey 會將代碼會滾到 Baseline 部分。函數
Chakra,Microsoft 的 JavaScript 引擎,也有着相似的兩個優化編譯器,SimpleJIT 和 FullJIT。解釋器將轉換後的代碼傳給 SimpleJIT(JIT,Just-In-Time),SimpleJIT 會將其加工成部分優化的代碼。再加上收集到的分析數據,FullJIT 就能夠生成高度優化的代碼。
JavaScriptCore(JSC),Apple 的 JavaScript 引擎,更是發揮到了極致,使用了三個不一樣的優化編譯器,Baseline、DFG 和 FTL。低級解釋器(LLInt)將轉換後的代碼傳給 Baseline 編譯器,經其加工後傳給 DFG(Data Flow Graph) 編譯器,進一步加工後,傳給 FTL(Faster Than Light) 編譯器。
爲何有些引擎的優化編譯器會比其餘引擎的多?這徹底是取捨問題。解釋器能夠很快地生成字節碼,可是字節碼的效率不高。優化編譯器雖然會花更長的時間,可是生成的機器碼更爲高效。是更快地去執行代碼,仍是花些時間去執行更優的代碼,這都是須要考慮的問題。有些引擎添加多種不一樣特色(省時或高效)的優化編譯器,雖然這會變得更加複雜,但卻能夠對以上的取捨有着更細粒度地控制。還有一點須要考慮的是,內存的使用。
以上只是強調了不一樣 JavaScript 引擎的解析器/編譯器的區別。拋開這些不談,從更高的層面來看,全部的 JavaScript 引擎有着相同的架構:一個解析器和一些解釋器/編譯器。
再來看看,在某些具體實現上,JavaScript 引擎之間還有哪些相同之處。
例如,JavaScript 引擎是如何實現 JavaScript 對象模型的?它們又是如何提高對象屬性訪問速度的?事實證實,全部主流的引擎在這點實現上都很是得類似。
ECMAScript 規範把全部的對象定義爲詞典,將字符串鍵映射到屬性特性(property attributes)。
除了 [[Value]]
, 規範還定義了一下屬性:
[[Writable]]
定義是否可寫。[[Enumerable]]
定義是否可枚舉。[[Configurable]]
定義是否可配置。[[雙中括號]]
是用來描述不能直接暴露給 JavaScript 的屬性。不過你依然能夠經過 Object.getOwnPropertyDescriptor
獲取某個對象上的以上屬性。
const object = { foo: 42 };
Object.getOwnPropertyDescriptor(object, 'foo');
// → { value: 42, writable: true, enumerable: true, configurable: true }
複製代碼
ok,這是 JavaScript 如何定義對象的。那麼,數組呢?
你能夠認爲數組是一個特殊的對象。一個不一樣點是,數組會對數組索引特殊處理。數組索引是 JavaScript 規範中的一個特殊術語。數組索引是某個範圍內的任何有效索引,即在 0 ~ 2³²−2 範圍內的任何一個整數。
另外一個不一樣點是,數組還有一個 length
屬性。
const array = ['a', 'b'];
array.length; // → 2
array[2] = 'c';
array.length; // → 3
複製代碼
在這個例子裏,數組建立好後,'length'
值爲 2。當咱們給數組索引爲 2 的位置賦值時,數組的 'length'
會自動更新。
在 JavaScript 中,數組的定義和對象很類似。例如,數組的全部的鍵(包括數組索引)都是字符串表示。數組的第一個元素存在鍵值爲 '0'
的地方。
另外一個屬性是 'length'
屬性,該屬性不可枚舉不可配置。
一旦數組添加一個元素,JavaScript 會自動更新 'length'
屬性上的 [[Value]]
值。
通常來講,數組的行爲也是和對象很是類似。
既然咱們知道在 JavaScript 中如何定義對象的。接下來讓咱們深刻了解 JavaScript 引擎是如何高效地處理對象的。
屬性訪問是最多見的一個操做,對 JavaScript 引擎來講,提高屬性訪問速度事件頗有意義的事。
const object = {
foo: 'bar',
baz: 'qux',
};
// Here, we’re accessing the property `foo` on `object`:
doSomething(object.foo);
// ^^^^^^^^^^
複製代碼
在 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 屬性,且用着字典這種數據結構:它包含字符串表示的鍵,而且鍵指向各自的屬性特性(property attributes)。
若是要訪問一個屬性,例如 object.y
,JavaScript 引擎會在 JSObject
中查找 y
,而後加載對應的屬性特性,最後返回 [[Value]]
。
可是在內存中,這些屬性特性要存儲在哪呢?咱們應該把它們看成 JSObject
的一部分存儲嗎?假設以後會有更多的擁有相同 Shape
的對象,若是咱們在 JSObject
上存儲一個包含屬性名稱和屬性特性的完整字典的話,那顯然會是一種浪費。由於擁有相同 Shape
的對象,它們的屬性名稱會重複。這會形成大量重複和沒必要要的內存使用。做爲優化,引擎將對象的 Shape
單獨地存儲。
Shape
包含全部的屬性名稱和屬性特性,除了 [[Value]]
。不過,Shape
包含了 [[Value]]
在 JSObject
上的偏移量,所以 JavaScript 引擎知道去哪裏找到相應的值。 每一個擁有相同 Shape
的 JSObject
都指向同一個 Shape
實例。如今,每一個 JSObject
只需存儲對象的值便可。
當咱們有不少個對象時,好處也是顯而易見的。無論有多少個對象,只要有相同的 Shape
,Shape
和屬性信息只須要存儲一次。
全部的 JavaScript 引擎都用 Shapes 來優化,但叫法卻不一樣:
在這篇文章中,咱們繼續稱之爲 Shapes。
若是一個對象有了一個肯定的 Shape
,而後又添加了一個屬性,這會發生什麼呢?JavaScript 引擎如何找到改變後的新 Shape
?
const object = {};
object.x = 5;
object.y = 6;
複製代碼
在 JavaScript 引擎中,這種 Shapes 結構稱之爲過渡鏈(transition chains)。以下:
對象開始時沒有任何屬性,所以它會指向一個空的 Shape
。下一條語句對象添加了一個屬性 'x'
,屬性值爲 5,所以對象指向包含屬性 'x'
的 Shape
,且在 JSObject
中偏移量爲 0 的位置添加 5。下一條語句對象添加了一個屬性 'y'
,屬性值爲 5,所以對象指向包含屬性 'x'
和 'y'
的 Shape
,且在 JSObject
中偏移量爲 1 的位置添加 6。
注意: 屬性的添加順序會影響
Shape
。例如,{x: 4, y: 5}
和{y: 5, x: 4}
有不一樣的Shape
。
咱們沒有必要讓每一個 Shape
都存儲完整的屬性表。相反,每一個 Shape
只須要知道新引入的屬性便可。例如,在這種狀況下,咱們沒有必要在最後一個 Shape
中存儲屬性 'x'
的信息,由於它能夠在鏈的上游中被查找到。要達此目的,每一個 Shape
都會和先前的 Shape
連接。
若是你在 JavaScript 代碼中寫了 o.x
,JavaScript 引擎會沿着過渡鏈查找屬性 'x'
,直到發現引入 'x'
的 Shape
。
可是,若是無法建立過渡鏈呢?例如,給兩個空對象添加不一樣的屬性。
const object1 = {};
object1.x = 5;
const object2 = {};
object2.y = 6;
複製代碼
這種狀況下,不得不進行分支處理,用過渡樹(transition tree)取代過渡鏈。
在這裏,咱們建立了一個空對象 a
並給它添加了屬性 'x'
。最終獲得以一個包含單個值的 JSObject
和兩種 Shape
(空的 Shape
和僅有屬性 'x'
的 Shape
)。
第二個例子也是以一個空對象 b
開始,可是添加的是屬性 'y'
。最終獲得兩條 Shape
鏈和三個 Shape
。
這是否意味着老是以空 Shape
開頭呢?不必定。
引擎對已經存在屬性的對象字面量作了優化。來看兩個例子,一個是從空的對象開始添加屬性 'x'
,一個是已經存在屬性 'x'
的對象字面量。
const object1 = {};
object1.x = 5;
const object2 = { x: 6 };
複製代碼
第一個例子中,咱們從空的 Shape
過渡到包含屬性 'x'
的 Shape
,就如以前所看到的那樣。
對於 object2
,它直接生成包含屬性 'x'
的對象而不是從空對象開始過渡。
這個包含屬性 'x'
的對象,以包含 'x'
的 Shape
開頭,省去了空 Shape
這個步驟。至少 V8 和 SpiderMonkey 是這麼作的。這種優化縮短了過渡鏈,使得建立對象更加高效。
Benedikt 的文章 surprising polymorphism in React applications 討論了這些微妙之處是如何影響到實際性能的。
這有一個擁有屬性 'x'、
'y'、
'z'` 的三維點對象的例子。
const point = {};
point.x = 4;
point.y = 5;
point.z = 6;
複製代碼
就如以前所學到的,在內存上,這會建立有三個 Shape
的對象(空 Shape
不計入)。訪問對象的屬性 'x'
,假如,你在程序中寫下了 point.x
,JavaScript 引擎會順着鏈表:它會從底部的 Shape
開始,一直向上查找,直到發現引入 'x'
的那個 Shape
。
若是這種操做很頻繁,就會顯得很慢,尤爲是一個對象有不少屬性時。檢索到須要的屬性所花時間是 O(n),即線性的。爲了提升檢索速度,JavaScript 引擎加入了 ShapeTable
數據結構。ShapeTable
是個字典,它將屬性和引入該屬性的 Shape
關聯起來。
且慢,咱們又回到了字典查找……這不就是咱們在引入 Shapes 以前的方式嗎?爲何咱們非要整出個 Shapes?
緣由是 Shapes 能夠實現另外一種稱之爲內聯緩存的優化。
ICs 是 JavaScript 快速運行的關鍵因素。JavaScript 引擎能夠利用 ICs 緩存對象的屬性信息,從而減小屬性查找的開銷。
有個函數 getX
,接受一個對象並加載該對象上的屬性 x
:
function getX(o) {
return o.x;
}
複製代碼
若是咱們在 JSC(JavaScriptCore) 中運行這個函數,它會生成如下的字節碼:
第一條指令(get_by_id
)是從參數 arg1
中加載屬性 x
,並將其值存儲到 loc0
中。第二條指令是返回 loc0
中存儲的值。
JSC 還在 get_by_id
指令中嵌入了內聯緩存,它是由兩個未初始化的插槽組成。
如今給函數 getX
傳入對象 { x: 'a' }
。如咱們所知,這個對象有一個包含屬性 x
的 Shape
,這個 Shape
存儲了屬性 x
的偏移量和特性。當咱們第一次執行函數時,get_by_id
指令會查找屬性 x
並檢索到值被存儲在偏移量爲 0
位置。
嵌在 get_by_id
指令中的內聯緩存會記住 Shape
和屬性的偏移量。
在下次函數執行時,內聯緩存會對比 Shape
,若是與以前的 Shape
相同,就只須要經過緩存的偏移量加載值。具體來講,若是 JavaScript 引擎發現對象的 Shape
和以前記錄的 Shape
同樣,那麼它就不再須要去查找屬性信息了 —— 屬性信息的查找就能夠徹底跳過。相比每次都去查找屬性信息,這樣的操做會顯著地提高速度。
對於數組,使用數組索引做爲數組的屬性是很常見的,屬性對應的值稱之爲數組元素。爲每一個數組的每一個數組元素存儲屬性特性是一種鋪張浪費的行爲。在 JavaScript 引擎中,數組的索引屬性默認是可讀、可枚舉和可配置的,且數組元素是與命名屬性分開存儲的。
思考如下這個數組:
const array = [
'#jsconfeu',
];
複製代碼
引擎存儲了一個數組長度爲 1 的數組,它指向一個包含偏移量和 length
特性的 Shape
。
這個以前見過的很類似…… 可是數組元素的值存在哪呢?
每一個數組都有一個獨立的元素備份存儲(elements backing store),包含着全部索引屬性對應的值。JavaScript 引擎沒必要爲數組元素存儲屬性特性,由於他們一般是可寫、可枚舉和可配置的,且數組索引能夠替代偏移量的做用。
若是是不尋常的狀況會怎樣呢?好比,改變數組元素的屬性特性(property attributes)。
// Please don’t ever do this!
const array = Object.defineProperty(
[],
'0',
{
value: 'Oh noes!!1',
writable: false,
enumerable: false,
configurable: false,
}
);
複製代碼
上面的這個代碼片斷是給對象屬性 '0'
的特性設置成非默認值。
像這種狀況,JavaScript 引擎會將整個元素備份存儲表示爲一個字典,把數組索引和屬性特性關聯起來。
即便數組中只有一個元素的屬性特性是非默認值,元素備份存儲也會進入緩慢低效的模式(從 Elements 模式 到 Dictionary Elements 模式)。避免用 Object.defineProperty
改變數組索引!
基於以上的知識,咱們可使用一些 JavaScript 編程技巧來提高性能:
Shape
。