原文連接:blog.logrocket.com/how-javascr…javascript
原文標題:How JavaScript works: Optimizing the V8 compiler for efficiency前端
本文首發於公衆號:符合預期的CoyPanjava
理解JavaScript的工做原理是寫出高效JavaScript代碼的關鍵。node
忘記那些可有可無的毫秒級改進:錯誤地使用對象屬性可能致使簡單的一行代碼速度下降7倍。chrome
考慮到JavaScript在軟件堆棧全部級別中的廣泛性,即便不是全部級別的基礎設施,也可能會出現微不足道的減速,而不只僅是網站的菜單動畫。編程
有許多的方法來編寫高效的JavasScript代碼,但在這篇文章裏面,咱們將着重介紹編譯器友好的優化方法,這意味着源代碼使編譯器優化變得簡單有效。後端
咱們將把討論範圍縮小到V8,即支持electron、node.js和google chrome的JavaScript引擎。爲了理解編譯器友好的優化,咱們首先須要討論JavaScript是如何編譯的。緩存
JavaScript在V8中的執行能夠分爲三個階段:網絡
第一個階段超出了本文的範圍,可是第二個和第三個階段對編寫優化的JavaScript有直接的影響。框架
咱們將討論這些優化方法以及代碼如何利用(或濫用)這些優化。經過了解JavaScript執行的基礎知識,您不只能夠理解這些性能方面的建議,還能夠學習如何發現本身的一些優化點。
實際上,第二和第三階段是緊密耦合的。這兩個階段在即時(just-in-time,JIT)範式中運行。爲了理解JIT的重要性,咱們將研究之前將源代碼轉換爲機器代碼的方法。
爲了執行任意一段程序,計算機必須將源代碼轉換成機器能夠運行的代碼。
有兩種方法能夠進行轉換。
第一種選擇是使用解釋器。解釋器能夠有效地逐行翻譯和執行。
第二種方法是使用編譯器。編譯器在執行以前當即將全部源代碼轉換爲機器語言。
下面,咱們將闡述兩種方法的優勢和缺點。
解釋器使用read-eval-print loop (REPL,交互式解釋器)的方式工做 —— 這種方式有許多的優勢:
然而,這些好處是以緩慢執行爲代價的:
(1)eval的開銷,而不是運行機器代碼。
(2)沒法跨程序的對各個部分進行優化。
更正式地說,解釋器在處理不一樣的代碼段時不能識別重複的工做。若是你經過解釋器運行同一行代碼100次,解釋器將翻譯並執行同一行代碼100次,沒有必要地從新翻譯了99次。
總結一下,解釋器簡單、啓動快,可是執行慢。
編譯器會在執行前翻譯全部的源代碼。
隨着複雜性的增長,編譯器能夠進行全局優化(例如,爲重複的代碼行共享機器代碼)。這爲編譯器提供了比解釋器惟一的優點 —— 更快的執行時間。
總結一下,編譯器是複雜的、啓動慢,可是執行快。
即時編譯器嘗試結合瞭解釋器和編譯器的優勢,使代碼轉換和執行都變得更快。
基本思想是避免重複轉換。首先,探查器會經過解釋器先跑一遍代碼。在代碼執行期間,探查器會跟蹤運行幾回的熱代碼段和運行不少次的熱代碼段。
JIT將熱代碼片斷髮送給基線編譯器,儘量的複用編譯後的代碼。
JIT同時將熱代碼片斷髮送給優化編譯器。優化編譯器使用解釋器收集的信息來進行假設,而且基於這些假設進行優化(例如,對象屬性老是以特定的順序出現)。
可是,若是這些假設無效,優化編譯器將執行 去優化,丟棄優化的代碼。
優化和去優化的過程是昂貴的。由此產生了一類JavaScript的優化方法,下面將詳細描述。
JIT須要存儲優化的機器代碼和探查器的執行信息等,天然會引入內存開銷。儘管這一點沒法經過優化的JavaScript來改善,但激發了V8的解釋器。
V8的解釋器和編譯器執行如下功能:
JIT編譯器顯示了開銷內存消耗。Ignition經過實現三個目標來解決這個問題:減小內存使用、減小啓動時間和下降複雜性。
這三個目標都是經過將AST轉換爲字節碼並在程序執行期間收集反饋來實現的。
AST和字節碼都會暴露給TurboFan。
在2008年發佈時,V8引擎最初直接將源代碼編譯爲機器代碼,跳過了中間字節碼錶示。在發佈時,V8就比競爭對手快了10倍。
然而,到今天,TurboFan接受了Ignition的字節碼,比它發佈的時候快了10倍。V8的編譯器通過了一系列的迭代:
根據Google慕尼黑技術講座(Titzer,3月16號),TurboFan優化了峯值性能、靜態類型信息使用、編譯器前端、中間和後端分離以及可測試性。最終沉澱出一個關鍵的貢獻:"節點海"。
在節點海中,節點表示計算,變表示依賴關係。
與控制流圖(CFG)不一樣的是,節點海能夠放寬大多數操做的評估順序。與CGF同樣,有狀態操做的控制邊和效果邊在須要時會約束執行順序。
Titzer進一步完善了這個定義,使之成爲一個節點湯,其中控制流子圖進一步放寬。這提供了許多優勢—例如,這避免了冗餘代碼的消除。
經過自下而上或自上而下的圖轉換,圖縮減被應用於這一系列節點。
TurboFan遵循4個步驟將字節碼轉換爲機器碼。請注意,如下管道中的優化是根據Ignition的反饋執行的。
TurboFan的在線JIT風格的編譯和優化意味着 V8從源代碼到機器代碼的轉換 結束了。
TurboFan的優化經過減輕糟糕的JavaScript的影響來提升JavaScript的網絡性能。然而,瞭解這些優化能夠提供進一步的加速。
下面是利用V8中的優化來提升性能的7個技巧。前四個重點是減小去優化。
更改對象屬性會產生新的隱藏類。以google i/o 2012中的如下示例爲例。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
var p1 = new Point(11, 22); // hidden class Point created
var p2 = new Point(33, 44);
p1.z = 55; // another hidden class Point created
複製代碼
正如你所見,p1和p2如今有不一樣的隱藏類了。這阻礙了TurboFan的優化嘗試:具體來講,任何接受Point對象的方法如今都是去優化的。
全部這些函數都使用兩個隱藏類從新優化。對對象形狀的任何修改都是如此。
更改對象屬性的順序會致使新的隱藏類,由於對象形狀中是包含順序的。
const a1 = { a: 1 }; # hidden class a1 created
a1.b = 3;
const a2 = { b: 3 }; # different hidden class a2 created
a2.a = 1;
複製代碼
上面的代碼中,a1和a2有不一樣的隱藏類。修復順序容許編譯器重用同一個隱藏類。由於添加的字段(包括順序)用於生成隱藏類的id
函數根據特定參數位置的值類型更改對象形狀。若是此類型發生更改,則函數將去優化並從新優化。
在看到四種不一樣的對象形狀後,該函數會變成megamorphic,TurboFan將不會再嘗試優化這個函數。
看下面這個例子:
function add(x, y) {
return x + y
}
add(1, 2); # monomorphic
add("a", "b"); # polymorphic
add(true, false);
add([], []);
add({}, {}); # megamorphic
複製代碼
第9行事後,TurboFan將不會再優化add這個函數。
不要在函數做用域中聲明類。如下面這個例子爲例:
function createPoint(x, y) {
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
return new Point(x, y);
}
function length(point) {
...
}
複製代碼
每一次createPoint這個函數被調用的時候,一個新的Point原型會被建立。
每個新的原型都對應着一個新的對象形狀,因此每一次length函數都會看到一個新的point的對象形狀。
跟以前同樣,當看到4個不一樣的對象形狀的時候,函數會變得megamorphic,TurboFan將不會再嘗試優化
length函數。
在腳本做用域中聲明class Point,咱們能夠避免每一次調用createPoint的時候,生成不一樣的對象形狀。
下一個tip是V8引擎裏的奇淫巧技。
for…in
這是V8引擎中的一個怪異行爲。這一特性以前包含在最初的Crankshaft裏面,後來被移植到了Ignition and Turbofan.
for…in
循環比函數迭代、帶箭頭函數的函數迭代和for循環中的object.keys快4-6倍。
接下來兩個Tip是對以前兩種說法的反駁。因爲現代V8引擎的改變,這兩種說法已經不成立了。
Crankshaft過去是使用一個函數的字節數來決定是否內聯一個函數的。而TurboFan是創建在AST上的,他使用AST節點的數量來決定函數的大小。
所以,無關的字符,好比空白,註釋,變量名長度,函數簽名等,不會影響函數的性能。
Try代碼塊之前容易出現高昂的優化-去優化的週期。現在,當在Try塊中調用函數時,turbofan再也不顯示出顯著的性能影響。
總之,優化方法一般集中在減小去優化和避免不可優化的megamorphic函數上。
經過對V8引擎框架的理解,咱們還能夠推斷出上面沒有列出的其餘優化方法,並儘量重用方法來利用內聯。如今您已經瞭解了JavaScript編譯及其對平常JavaScript使用的影響。