1、V8的演進歷史
2008年V8釋出第一個版本,當時的V8架構比較激進,直接將js程式碼編譯為機器碼並執行,所以執行速度很快,但是隻有Codegen一個編譯器,所以對程式碼的優化很有限。
2010年V8釋出了Crankshaft編譯器,js程式碼會先被Full-Codegen編譯器編譯,如果後續改程式碼塊會被多次執行,則會用Crankshaft編譯器重新編譯,生成更優化的程式碼,之後就使用優化後的程式碼來執行,進而提升效能。
Crankshaft編譯器對程式碼的優化有限,所以2015年V8中加入了TurboFan編譯器,此時V8依舊是直接將原始碼編譯為機器碼執行,這種架構存在一個核心問題,記憶體消耗特別大(通常一個幾KB的檔案,轉換為機器碼可能就是幾十MB,這會小號巨大的記憶體空間)。
2016年V8加入了Ignition編譯器,重新引入位元組碼,旨在減少記憶體使用。
2017年V8正式釋出全新編譯pipeline,它使用Ignition和TurboFan的組合來編譯執行程式碼,從這(V8的5.9版本)開始,早期的Full-Codegen和Crankshaft編譯器不再用來執行js,在最新的架構中,最核心的模組有三個:解析器(Parser)、直譯器(Ignition)、優化編譯器(TurboFan)。
當V8執行js原始碼時,首先,解析器會把原始碼解析為抽象語法樹(Abstract Syntax Tree),直譯器再將AST翻譯為位元組碼,一邊解釋一邊執行,在此過程中,直譯器會記錄特定程式碼片段的執行次數,如果執行次數超過了某個閾值,該段程式碼就被標記為熱程式碼(hot code),並將執行資訊反饋給優化編譯器(TureboFan),優化編譯器根據反饋資訊,優化並編譯位元組碼,最終生成優化後的機器碼,這樣,當該段程式碼再次被執行時,直譯器就直接使用優化後的機器碼執行,不用再次解釋,從而大大提高了程式碼執行效率,這種在執行時編譯程式碼的技術叫即時編譯(JIT)。
2、V8的解析器
將js原始碼解析為AST,此過程會經過詞法分析、語法分析,通過預解析提高執行效率。
詞法分析:將js原始碼解析為一個個最小單元的token。
在V8中,Scanner負責接收Unicode字元流,並將其解析為tokens提供給解析器使用。
語法分析:根據語法規則,將tokens組成一個具有前臺層級的抽象語法樹,在這個過程中,如果原始碼不符合語法規範,解析過程就會終止,並丟擲語法錯誤。
對於一份js原始碼,如果所有原始碼都要經過解析才能執行,那必然會面臨三個問題:1、一次性解析所有程式碼,程式碼執行時間變長,2、記憶體消耗增加,因為解析完的AST以及根據AST編譯後的位元組碼都會存放在記憶體中,3、佔用磁碟空間,編譯後的程式碼會快取在磁碟上。
因此,現在主流的瀏覽器都會進行延遲解析,在解析過程中,對於不是立即執行的函式,只進行預解析(Pre Parser),只有當函式呼叫時才對函式進行全量解析。進行預解析時,只驗證函式的語法是否有效,解析函式宣告,確定函式作用域,不生成AST。實現預解析的就是Pre-Parser解析器。
3、V8的直譯器
Js原始碼轉換為CPU可識別的機器碼,需要消耗巨大的記憶體,V8為了解決記憶體記憶體佔用問題引入了位元組碼。位元組碼是對機器碼的抽象,語法與彙編有些類似,可以把它看做一個一個的指令。
解析器Ignition根據AST生成位元組碼並執行。
這個過程中會收集反饋資訊,交給TurboFan進行優化編譯。TurboFan根據Ignition收集的反饋資訊,將位元組碼編譯為優化後的機器碼,後續Ignition有優化後的機器碼代替位元組碼執行。
4、V8的優化編譯器
Ignition直譯器在執行位元組碼時,依舊需要將位元組碼轉換為機器碼,因為CPU只能識別機器碼,雖然多了一層位元組碼的轉換,看起來效率低了,但是相比於機器碼,基於位元組碼可以更方便的進行效能優化,其中最主要的優化就是使用TurboFan編譯器編譯熱點程式碼。Ignitio直譯器在解釋執行的過程中,會標記重複執行的熱點程式碼,這些被標記的程式碼,會被TurboFan編譯器編譯生成效率更高的機器碼。
TurboFan在工作的時候主要用到了兩個演算法,一個內聯,一個是逃逸分析。
內聯就是對巢狀函式進行內聯分析,如下圖左側程式碼,如果不經優化,直接編譯該段程式碼,則會生成兩個函式的機器碼,但為了進一步提升效能,TurboFan就會對這兩個函式進行內聯,然後在編譯,如下提中間程式碼,更進一步,由於函式內部變數的值都是確定的,所以函式還可以進一步優化,如下圖右側程式碼。最終生成的機器碼相比優化前少了非常多,執行效率自然也就高了。通過內聯,可以降低複雜度,消除冗餘程式碼,合併常量,並且,內聯技術通常也是逃逸分析的基礎。
逃逸分析是分析物件的生命週期是否僅限於當前函式,如果物件是在函式內部定義的,且物件只作用於函式內部,比如物件沒有被返回,也沒有傳遞或者給其他函式呼叫,此時,這個物件會被認為是”未逃逸”的。在編譯優化時,會使用標量替換掉未逃逸的物件,以減少物件定義,從而減少從記憶體中訪問物件屬性,提升了執行效率的同時,還減少了記憶體的使用。
文章來源於視訊:https://www.zhihu.com/zvideo/...