摘要:縱覽各個引擎的實現,我們發現基于字節碼的實現是主流。引入字節碼之后,的性能得到了顯著的提升。而這次引入字節碼卻是向著相反的方向后退。這便是引入字節碼的主要動機。如今也回到了字節碼的懷抱,不禁令人感嘆引擎與字節碼真是有著不解之緣
首先貼個Javascript性能測試站點,測試并展示了數個 JavaScript 引擎的性能數據:arewefastyet
我們看到在這個比武場上,最近 Chrome 出現了多個新條目,其中很多條目都是關于 v8 的 Ignition 新架構的組合,他們是 v8 引擎最近推出的 JS 字節碼解釋器。
縱覽各個 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 引入字節碼的動機又是什么呢?筆者總結下來有三條:
(主要動機)減輕機器碼占用的內存空間,即犧牲時間換空間
提高代碼的啟動速度
對 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 腳本的解析結果,不該花費這么久。這是為什么呢?
這就是之前 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 引擎與字節碼真是有著不解之緣!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/83059.html
摘要:摘要性能彪悍的引擎。深入淺出系列深入淺出第課箭頭函數中的究竟是什么鬼深入淺出第課函數是一等公民是什么意思呢深入淺出第課什么是垃圾回收算法深入淺出第課是如何工作的最近,生態系統又多了個非常硬核的項目。 摘要: 性能彪悍的V8引擎。 《JavaScript深入淺出》系列: JavaScript深入淺出第1課:箭頭函數中的this究竟是什么鬼? JavaScript深入淺出第2課:函數是一...
摘要:引擎可以是一個標準的解釋器,也可以是一個將編譯成某種形式的字節碼的即時編譯器。和其他引擎最主要的差別在于,不會生成任何字節碼或是中間代碼。不使用中間字節碼的表示方式,就沒有必要用解釋器了。 原文地址:https://blog.sessionstack.com... showImg(https://segmentfault.com/img/bVVwZ8?w=395&h=395); 數周之...
摘要:類將源代碼解釋并構建成抽象語法樹,使用類來創建它們,并使用類來分配內存。類抽象語法樹的訪問者類,主要用來遍歷抽象語法樹。在該函數中,先使用類來生成抽象語法樹再使用類來生成本地代碼。 通過上一篇文章,我們知道了JavaScript引擎是執行JavaScript代碼的程序或解釋器,了解了JavaScript引擎的基本工作原理。我們經常聽說的JavaScript引擎就是V8引擎,這篇文章我們...
摘要:最后,客戶端只是依賴于引擎的環境之一。新的編譯器管道利用來實現,并生成可以轉換生成器控制流到簡單的本地控制流的字節碼。可以更容易地優化所得到的字節碼,因為它不需要知道關于生成器控制流的任何具體內容,只是如何保存和恢復函數的狀態。 本文轉載自:眾成翻譯譯者:smartsrh鏈接:http://www.zcfy.cc/article/2978原文:https://v8project.blo...
摘要:第二篇文章將深入谷歌的引擎的內部。引擎可以實現為標準解釋器,或者以某種形式將編譯為字節碼的即時編譯器。這個引擎是在谷歌中使用的,但是,與其他引擎不同的是也用于流行的。一種更復雜的優化編譯器,生成高度優化的代碼。不是唯一能夠做到的引擎。 本系列的 第一篇文章 主要介紹引擎、運行時和調用堆棧。第二篇文章將深入谷歌 V8 的JavaScript引擎的內部。 想閱讀更多優質文章請猛戳GitHu...
閱讀 3546·2021-11-22 15:22
閱讀 3335·2019-08-30 15:54
閱讀 2730·2019-08-30 15:53
閱讀 820·2019-08-29 11:22
閱讀 3541·2019-08-29 11:14
閱讀 2082·2019-08-26 13:46
閱讀 2217·2019-08-26 13:24
閱讀 2281·2019-08-26 12:22