V8 Ignition:JS 引擎與字節碼的不解之緣(轉載)

首先貼個Javascript性能測試站點,測試並展現了數個 JavaScript 引擎的性能數據:arewefastyet
圖片描述
咱們看到在這個比武場上,最近 Chrome 出現了多個新條目,其中不少條目都是關於 v8 的 Ignition 新架構的組合,他們是 v8 引擎最近推出的 JS 字節碼解釋器。html

縱覽各個 JS 引擎的實現,咱們發現基於字節碼的實現是主流。例如蘋果公司的 JavaScriptCore (JSC) 引擎,2008 年時他們引入了 SquirrelFish(市場名 Nitro),實現了一個字節碼寄存器機(Register Machine)。再如 Mozilla 公司的 SpiderMonkey,他們使用字節碼的歷史更久,能夠追溯到 1998 年的 Netscape 4(見 https://dxr.mozilla.org/class... ),SpiderMonkey 實現的是堆棧機(Stack Machine)。微軟的 Chakra 也使用了字節碼,他們實現的是寄存器機(Register Machine)。而 v8 以前的作法是比較「脫俗」的,他們跳過了字節碼這一層,直接把 JS 編譯成機器碼。而在剛剛過去的五一假日前夕,v8 5.9 發佈了,其中的 Ignition 字節碼解釋器將默認啓動 :https://v8project.blogspot.co... 。v8 自此回到了字節碼的懷抱。前端

這讓筆者不由懷念起 2007 年 Ruby 1.9 的發佈。當時 Ruby 1.9 也是第一次引入了字節碼,名爲 YARV,由笹田耕一領導主導開發完成。當時,Ruby 還在使用松本行弘的初級的解釋器實現,亦即,解釋器每次遍歷代碼的抽象語法樹(AST)來進行 Ruby 代碼的解釋執行。而 YARV 則把抽象語法樹(AST)先編譯成字節碼,而後再運行。引入字節碼以後,Ruby 的性能獲得了顯著的提高。瀏覽器

而此次 V8 引入字節碼倒是向着相反的方向後退。由於以前 v8 選擇了直接將 JS 代碼編譯到機器代碼執行,機器碼的執行性能已經很是之高,而此次引入字節碼則是選擇編譯 JS 代碼到一箇中間態的字節碼,執行時是解釋執行,性能是低於機器代碼的。最終的性能測試勢必會下降,而不是提升。那麼 V8 爲何要作這樣一個退步的選擇呢?爲 V8 引入字節碼的動機又是什麼呢?筆者總結下來有三條:緩存

  1. (主要動機)減輕機器碼佔用的內存空間,即犧牲時間換空間網絡

  2. 提升代碼的啓動速度閉包

  3. 對 v8 的代碼進行重構,下降 v8 的代碼複雜度架構

故事得從 Chrome 的一個 bug 提及: http://crbug.com/593477 。Bug 的報告人發現,當在 Chrome 51 (canary) 瀏覽器下加載、退出、從新加載 facebook 屢次,並打開 about:tracing 裏的各項監控開關,能夠發現第一次加載時 v8.CompileScript 花費了 165 ms,再次加載加入 V8.ParseLazy 竟然依然花費了 376 ms。按說若是 Facebook 網站的 js 腳本沒有變,Chrome 的緩存功能應該緩存了對 js 腳本的解析結果,不應花費這麼久。這是爲何呢?ide

這就是以前 v8 將 JS 代碼編譯成機器碼所帶來的問題。由於機器碼佔空間很大,v8 沒有辦法把 Facebook 的全部 js 代碼編譯成機器碼緩存下來,由於這樣不只緩存佔用的內存、磁盤空間很大,並且退出 Chrome 再打開時序列化、反序列化緩存所花費的時間也很長,時間、空間成本都接受不了。函數

因此 v8 退而求其次,只編譯最外層的 js 代碼,也就是下圖這個例子裏面綠色的部分。那麼內部的代碼(以下圖中的黃色、紅色的部分)是何時編譯的呢?v8 推遲到第一次被調用的時候再編譯。這時間上的推移還致使另一個短板,就是代碼必須被解析屢次——綠色的代碼一次、黃色的代碼再解析一次(當 new Person 被調用)、紅色的代碼再解析一次(當 doWork() 被調用)。所以,若是你的 js 代碼的閉包套了 n 層,那麼最終他們至少會被 v8 解析 n 次。
圖片描述性能

Facebook 的網站之因此收到這個設計帶來的負面的性能影響,就是由於他們的前段工程流程中最後把各個獨立的 module 編譯成了一個單獨的文件,其中用到了不少閉包,如:
圖片描述
如此一來 Chrome 的緩存做用就只能做用在最外層的 __d() 代碼上,而內部的真正的邏輯根本沒有被緩存。

剛纔提到了機器碼佔空間大的一個壞處,就是不能一次性編譯所有的代碼。機器碼佔空間大還有另一個壞處,就是一些只運行一次的代碼浪費了寶貴的內存資源。正如上面 Facebook 中的 __d() 系列函數,他們的做用可能只是註冊、初始化各個模塊組件,而一旦初始化完成便不會再執行。但因爲機器碼佔空間大,這些只執行一次的代碼也會在內存中長期存在、長期佔用空間。正以下圖所示,通常狀況下大約 30% 的 V8 堆空間都用來存儲未優化的機器碼。
圖片描述

而引入字節碼以後,佔空間的問題就能夠獲得緩解。經過恰當地設計字節碼的編碼方式,字節碼能夠作到比機器碼緊湊不少。V8 引入 Ignition 字節碼後,代碼的內存佔用確實下降了,以下圖所示。
圖片描述

經過對十大流行手機端網站的測試,能夠發現他們的內存佔用顯著降低。
圖片描述
這即是 v8 引入字節碼的主要動機。而這樣實現以後其實順便又帶來了兩個好處,筆者認爲能夠視做 v8 引入字節碼的次要動機,亦即:更快的啓動速度和更好的 v8 代碼重構。

在啓動速度方面,現在內存佔用過大的問題消除了,就能夠提早編譯全部代碼了。由於前端工程爲了節省網絡流量,其最終 JS 產品每每不會分發無用的代碼,因此能夠指望所有提早編譯 JS 代碼不會由於編譯了過多代碼而浪費資源。v8 對於 Facebook 這樣的網站就能夠選擇所有提早編譯 JS 代碼到字節碼,並把字節碼緩存下來,如此 Facebook 第二次打開的時候啓動速度就變快了。下圖是舊的 v8 的執行時間的統計數據,其中 33% 的解析、編譯 JS 腳本的時間在新架構中就能夠被縮短。
圖片描述
v8 自身的重構方面,有了字節碼,v8 能夠朝着簡化的架構方向發展,消除 Cranshaft 這個舊的編譯器,並讓新的 Turbofan 直接從字節碼來優化代碼,並當須要進行反優化的時候直接反優化到字節碼,而不須要再考慮 JS 源代碼。最終達到以下圖所示的架構。
圖片描述
其實,Ignition + TurboFan 的組合,就是字節碼解釋器 + JIT 編譯器的黃金組合。這一黃金組合在不少 JS 引擎中都有所使用,例如微軟的 Chakra,它首先解釋執行字節碼,而後觀察執行狀況,若是發現熱點代碼,那麼後臺的 JIT 就把字節碼編譯成高效代碼,以後便只執行高效代碼而再也不解釋執行字節碼。蘋果公司的 SquirrelFish Extreme 也引入了 JIT。SpiderMonkey 更是如此,全部 JS 代碼最初都是被解釋器解釋執行的,解釋器同時收集執行信息,當它發現代碼變熱了以後,JaegerMonkey、IonMonkey 等 JIT 便登場,來編譯生成高效的機器碼。

回顧歷史,不少 JS 引擎都是採用了字節碼這一腳本語言實現技術的,而 v8 一枝獨秀,走「純機器碼」路線,其實過於激進了:雖然執行性能上能夠登峯造極,但卻帶來了內存佔用過大的問題。此次引入字節碼實則是作了工程上的恰當取捨,將損失掉的內存找回來,更加符合現在移動和嵌入式設備爲主的應用場景;以時間換空間,讓 v8 能更好的服務於低內存的設備。現在 V8 也回到了字節碼的懷抱,不由使人感嘆 JS 引擎與字節碼真是有着不解之緣!

相關文章
相關標籤/搜索