走向核心的探索 — V8引擎初探

前言

這段時間在寫編譯原理的課設,對於編譯器的實現算是入了個門,着就激起了我心中的一個本源問題,JavaScript的引擎究竟是什麼樣子的,V8直接致使了Node.js時代,JavaScript能作的事情愈來愈多。做爲一個出色的JavaScript引擎,他的模式值得咱們思考和學習。前端

那麼V8引擎究竟是怎麼工做的呢?git

兩個編譯器的故事

V8會編譯全部JavaScript到原生代碼,而在V8中,有兩個編譯器在運行着:一個運行比較快,輸出着通常的代碼,另外一個運行的沒有那麼快,可是盡力的輸出着優化過的代碼。github

第一個編譯器 — full-codegen 編譯器

輸出通常代碼的那個編譯器在內部被稱爲:full-codegen(全代碼生成) 編譯器。它接受一個函數的抽象語法樹,遍歷語法樹,直接生產彙編代碼。經過獲取被解析過的函數源代碼(抽象語法樹),帶有類型記錄緩存的原生代碼。它是一個很通常的編譯器,運行了通常編譯器從語法分析後直到代碼生成的過程。後端

全部本地的變量沒有被存放在寄存器中,而是都放在了棧或者堆裏面。全部被嵌套函數引用的變量所有被存在了堆裏面,這個堆決定了在函數上下文中,哪些函數被定義了。編譯器會根據狀況把這些值放進寄存器裏面,並執行具體工做。而對於存在棧裏面的變量,棧頂的幾個變量會暫時的在寄存器內緩存。而更復雜的狀況,則有實時處理程序來管理。這個編譯器會記錄語句執行的上下文,這樣就能直接跳到須要執行的塊,而不是把變量放進寄存器,測試這個量是否是0,而後產生分支(大概就是彙編裏面的TEST,JNZ的步驟吧)。相似於簡單的算數求值也會在這裏被優化進行。緩存

這個編譯器使用了一個很是重要的技術來優化代碼—— inline caching。編譯的時候有這樣的緩存,直接能夠用於賦值、一元運算、二元運算、函數調用、屬性獲取還有比較值。inline caching 還向另外一個優化編譯器提供了類型源數據。而inline caching 在編譯的時候,緩存了鍵和值的儲存,而通常的操做並不會觸發inline caching。bash

類型反饋

當V8引擎第一次看到一個函數的時候,他只直接創建語法樹,不作其餘事情。直到第一次調用函數的時候,V8才第一次跑full-codegen 編譯。但這種有點偷懶的作法,在代碼開始運行後有了變化。運行開始後,會觸發分析線程,這個線程負責看看代碼跑的怎麼樣,那些函數是熱點函數。函數

這種偷懶,靜觀其變的作法,讓V8引擎能夠跟蹤類型變化,記錄相應數據。當V8發現了熱點函數,以爲這個函數能夠幫一把的時候,他就把類型反饋數據給編譯器。運行時的類型反饋數據會被記錄。性能

Unknown
           |   \____________
           |                |
      Primitive       Non-primitive
           |   \_______     |
           |           |    |
        Number       String |
         /   \         |    |
    Double  Integer32  |   /
        |      |      /   /
        |     Smi    /   /
        |      |    / __/
        Uninitialized.
複製代碼

每次看到新的值,就計算這個值的類型,而後和舊的值類型進行運算。最初的變量類型是Uninitialized(未初始化)。因此當看到一個整型的時候,若是他的大小在Smi (small integer) 範圍內的時候,會直接在類型反饋裏面推斷,它是一個Smi。可是當看到這個值變成了Double了,那麼作運算後,這個值的推斷直接變爲Number。推斷每次的結果就是尋找了兩個值的最近共同parent。在內部作了類型預估,讓編譯器能夠有目的的優化。學習

類型反饋數據和抽象語法樹是相互聯繫的,函數的熱度是由一個整型記錄的,相應的從full-codegen獲取熱點節點標記信息,並把這些信息送給編譯器作進一步優化。測試

到了這裏,這個過程開始變得有一些複雜了。這個過程裏面須要實現對於編譯器棧的向上向下兼容。編譯器須要得到操做數和結果的類型反饋,而且還要能準確的找到這個數據。而後你還要可以把這些東西從新和抽象語法樹關聯起來,編譯器才能從語法樹有目的優化代碼。

V8在這個過程上,經過把數據分析成TypeFeedbackOracle對象,而且把這個對象和特定的語法樹節點聯繫。最終,V8經過訪問語法樹的節點就會經過這個對象進行,這個對象也可以優化編譯過程。

第二個編譯器 — crankshaft 編譯器

一旦V8肯定了熱點函數,獲得了類型反饋的信息,他就會嘗試帶着這些信息來運行優化編譯器。優化編譯器在市面上稱爲crankshaft(軸心) 編譯器,雖然在源代碼上面並無這樣命名。實際上,crankshaft 編譯器在源代碼裏面是由四個過程組成的:帶有類型反饋的抽象語法樹->高級別中間代碼->低級別中間代碼->優化過的原生代碼。

高級別中間代碼是編譯器前端造成的代碼,而低級別中間代碼是後端使用的中間代碼,經過前端後端雙重的優化,讓V8引擎對熱點函數有更好的處理。

總結

V8引擎採用惰性優化的方式來提升性能,經過針對運行時的熱點函數優化,快編譯和慢優化的結合,並且經過合理的類型推斷解決的JavaScript的類型問題。這只是對V8早期版本的一個概念分析,可是我已經開始接觸到了V8優化的魔法。

參考資料:

http://wingolog.org/archives/2011/07/05/v8-a-tale-of-two-compilers#ffc2b5d74c27fa60d75658244fee88e6fa783afb

https://github.com/v8/v8/tree/master/src

相關文章
相關標籤/搜索