幾個星期前,我們開始了深入瞭解JavaScript及實際是如何運作的系列文章,我們認為通過了解JavaScript的構建模組以及它們如何共同發揮作用,您將能夠編寫更好的程式碼和應用程式。
概覽
JavaScript引擎是一個程式或執行JavaScript程式碼的直譯器。JavaScript引擎可以理解為標準直譯器,或執行時編譯器,它以某種形式將JavaScript編譯為位元組碼。
- V8——由Google開發的開源軟體,用C ++編寫
- Rhino——由Mozilla Foundation管理,開源,完全用Java開發
- SpiderMonkey ——第一個支援Netscape Navigator的JavaScript引擎,現在支援Firefox
- JavaScriptCore——開源,以Nitro銷售,由Apple為Safari開發
- KJS - KDE的引擎,最初由Harri Porten為KDE專案的Konqueror Web瀏覽器開發
- Chakra (JScript9) ——Internet Explorer
Chakra (JavaScript) ——Microsoft Edge
Nashorn——由甲骨文Java語言和工具組開源作為OpenJDK的一部分
JerryScript ——是物聯網的輕量級引擎
V8為什麼被創造出來?
V8最初設計旨在web瀏覽器內部執行JavaScript的效能提升,為了增加執行速度,V8沒有把JavaScript程式碼轉化成更有效的機器碼,而不是使用直譯器。像許多現代JavaScript引擎一樣,如SpiderMonkey或Rhino(Mozilla),它通過實現JIT(即時)編譯器將JavaScript程式碼編譯成機器程式碼。這裡的主要區別是V8不產生位元組碼或任何中間程式碼。
V8曾經有兩個編譯器
在V8版本5.9出現之前(今年早些時候釋出的),該引擎使用了兩個編譯器:
- full-codegen——一個簡單而快速的編譯器,可以生成簡單但未被優化的機器程式碼。
- Crankshaft——一種更復雜的(即時)優化編譯器,可生成高度優化的程式碼。
在V8引擎裡面也使用了多個執行緒:
- 主執行緒:獲取程式碼,編譯程式碼然後執行它
- 還有一個被用來編譯的單獨的執行緒,因此主執行緒可以繼續執行,而它也同時可以優化程式碼
- 一個分析執行緒,它將告訴執行時哪些方法耗費了大量的時間,以便Crankshaft可以優化它們
- 一些執行緒作用是處理掃描垃圾收集器
當JavaScript程式碼首次執行的時候,V8利用full-codegen直接將解析後的JavaScript轉換為機器程式碼而無需其他中間過程的任何轉換。這使它可以非常快速地開始執行機器程式碼。請注意,V8不使用中間位元組碼錶示,因此無需直譯器。
當你的程式碼執行了一段時間之後,這個分析執行緒已經收集了足夠多的資料來告訴應該優化哪個方法。
接下來,Crankshaft優化從另一個執行緒開始,它把JavaScript抽象語法樹轉化為名為Hydrogen的高階靜態單賦值(SSA)表示,並嘗試優化Hydrogen圖表,大多數優化都是在這個級別完成的。
內聯
隱藏類
JavaScript是一門基於原型的語言,它沒有建立類,物件被建立是基於引用的,JavaScript也是一種動態程式語言,這意味著可以在例項化後輕鬆地在物件中新增或刪除屬性。
大多數的JavaScript解析器使用類似字典的結構(基於雜湊函式)來儲存物件屬性值在記憶體當中的位置,這個結構使得在JavaScript中檢索屬性的值比java或C#等非動態程式語言中的計算成本更高,在Java當中,所有物件屬性都是在編譯之前由固定物件模版確定的,並且無法在執行時動態新增或刪除(C#具有動態性型別,這是另一個主題),結果,屬性值(或指向這些屬性的指標)可以作為連續緩衝區儲存在記憶體中,每個緩衝區之間具有固定偏移量,可以根據屬性型別輕鬆確定偏移的長度。而在執行時可以更改屬性值的JavaScript中,這是不可能的。
由於使用字典結構去查詢屬性值在記憶體當中的位置是非常低效的,V8使用來一個不同的方法去替代:隱藏類。隱藏類的作用類似於在Java語言中的固定物件模版(Classes),除非它們是在執行時建立的。讓我們看看它們實際上是什麼樣的:
function Point(x, y) {
this.x = x;
this.y = y;
}var p1 = new Point(1, 2);複製程式碼
一旦這個new Point(1,2)
呼叫發生,V8將建立一個名為“C0”的隱藏類。
尚未為Point定義任何屬性,因此“C0”為空。
一旦第一行程式碼this.x = x
被執行(Point方法裡面),V8將建立第二個基於“C0”的隱藏類“C1”,“C1”描述了可以找到屬性x在記憶體中的位置(相對於物件指標),這種情況下,“x”被儲存在偏移0處,這意味著當將記憶體中的Point物件視為連續緩衝區時,第一偏移位置將對應於屬性“x”。V8還將使用“類轉換”更新“C0”,類轉換表示如果將屬性“x”新增到Point物件,則隱藏類應從“C0”切換到“C1”。下面的Point物件的隱藏類現在是“C1”。
每次將新屬性新增到物件時,舊的隱藏類都會被更新到指向新隱藏類的轉換路徑。隱藏類轉換非常重要,因為它們允許在以相同方式建立的物件之間共享隱藏類(比如例項化兩個Point物件,他們的共同隱藏類是C0)。如果兩個物件共享一個隱藏類並且同一屬性被新增到它們中,則轉換將確保兩個物件都接收相同的新隱藏類(比如都新增“x”屬性,就會都指向C1)以及所有的優化程式碼
當“this.y=y”被執行的時候,這個過程是重複進行的(Point函式裡面的“this.y=y”),如果屬性“y”被新增到Point上,類轉換將會基於“C1”生成“C2”隱藏類,point物件的隱藏類將會更新到“C2”。
隱藏類轉換是決於屬性新增到物件的順序,看下下面的程式碼:
function Point(x, y) {
this.x = x;
this.y = y;
}var p1 = new Point(1, 2);
p1.a = 5;
p1.b = 6;var p2 = new Point(3, 4);
p2.b = 7;
p2.a = 8;複製程式碼
內聯快取
V8優化動態型別語言的另一種方法稱為內聯快取,內聯快取依賴於觀察到對相同方法的重複呼叫往往發生在同一型別的物件上。可以在此處找到對內聯快取的深入解釋。
那麼它是怎樣工作的?
那麼隱藏類和內聯快取是如何相關的概念又是怎樣的呢?
內聯快取也是為什麼相同型別的物件共享隱藏類非常重要的原因。
如果你建立兩個相同型別和不同隱藏類的物件(正如我們之前的例子中所做的那樣),V8將無法使用內聯快取,因為即使這兩個物件屬於同一型別,它們對應的隱藏類也會對其屬性分配不同的偏移量。
這兩個物件基本相同,但“a”和“b”屬性是按不同順序建立的。
編譯到機器程式碼
垃圾回收
對於垃圾回收,V8是使用了傳統的分代式標記清除垃圾回收機制來清除老一代,標記階段JavaScript會停止執行,為了控制GC(垃圾回收)的成本和程式碼執行的穩定,V8是用來增量標記:和遍歷整個堆、試圖示記每一個可能的物件不同,它只是標記堆的一部分,然後恢復正常的執行,下一次GC將從上一次停止的地方繼續遍歷,在執行的時間段裡,它允許短暫的暫停,如前文所說,這個清除階段在單獨的執行緒中進行的。
隨著2017年早些時候V8 5.9版本的釋出,一個新的執行管線被引入,這個新的管線在實際的JavaScript引用程式中實現了更大的效能提升和顯著的記憶體節省。
這個新的管線是在V8的直譯器Ignition和V8最新的優化編譯器TurboFan之上構建的,
你可以在此檢視V8團隊有關該主題的博文。
自從V8的5.9版本釋出之後,由於V8團隊力爭和新的JavaScript語言特性以及針對這些新特性所需要的優化保持一致,full-codegen和Crankshaft(這兩項技術從2010年開始為V8服務)不再被V8用來執行JavaScript。
這意味著整個V8將擁有更簡單和更易維護的架構。
Web和Node.js基準上的改進
這些優化只是剛剛開始,新的Ignition和TurboFan管線為未來的優化鋪平了道路,未來JavaScript的效能會有更加巨大的提升,並能讓V8在Chrome和Node.js中節約資源。
最後,這裡提供一些小技巧,幫助大家寫出更優化的、更優質的JavaScript。從上文中您一定可以輕鬆地總結出一些技巧,不過為了方便,仍然為您提供一份總結。
怎麼寫出最佳JavaScript程式碼
1.物件屬性的順序:永遠用相同的順序為您的物件屬性例項化,這樣隱藏類和隨後的優化程式碼才能共享。
2.動態屬性:在物件例項化後為其新增屬性會導致隱藏類變化,從而會減慢為舊隱藏類所優化的方法的執行。所以,儘量在建構函式中分配物件的所有屬性。
3.方法:重複執行相同方法的程式碼會比不同的方法只執行一次的程式碼執行得更快(由於內聯快取的原因)。
4.陣列:避免使用keys不是遞增數字的稀疏陣列(sparse arrays)。並不為每個元素分配記憶體的稀疏陣列實質上是一個hash表。這種陣列中的元素比通常陣列的元素會花銷更大才能獲取到。此外,避免使用預申請的大型陣列。最好隨著需要慢慢增加陣列的大小。最後,不要刪除陣列中的元素,因這會使得keys變得稀疏。
5.標記值:V8用32個位元來表示物件和數字。它使用1個位元來區分是一個物件(flag = 1)還是一個整型(flag = 0)(被稱為SMI或SMall Integer,小整型,因其只有31位元來表示值)。然後,如果一個數值大於31位元,V8就會給這個數字進行裝箱操作(boxing),將其變成double型,並建立一個新的物件將這個double型數字放入其中。所以,為了避免代價很高的boxing操作,儘量使用31位元的有符號數。
後續文件翻譯會陸續跟進!!
歡迎關注玄說前端公眾號,後續將推出系列文章《一個大型圖形化應用0到1的過程》,此賬戶也將同步更新