摘要:縱覽各個引擎的實現(xiàn),我們發(fā)現(xiàn)基于字節(jié)碼的實現(xiàn)是主流。引入字節(jié)碼之后,的性能得到了顯著的提升。而這次引入字節(jié)碼卻是向著相反的方向后退。這便是引入字節(jié)碼的主要動機。如今也回到了字節(jié)碼的懷抱,不禁令人感嘆引擎與字節(jié)碼真是有著不解之緣
首先貼個Javascript性能測試站點,測試并展示了數(shù)個 JavaScript 引擎的性能數(shù)據(jù):arewefastyet
我們看到在這個比武場上,最近 Chrome 出現(xiàn)了多個新條目,其中很多條目都是關(guān)于 v8 的 Ignition 新架構(gòu)的組合,他們是 v8 引擎最近推出的 JS 字節(jié)碼解釋器。
縱覽各個 JS 引擎的實現(xiàn),我們發(fā)現(xiàn)基于字節(jié)碼的實現(xiàn)是主流。例如蘋果公司的 JavaScriptCore (JSC) 引擎,2008 年時他們引入了 SquirrelFish(市場名 Nitro),實現(xiàn)了一個字節(jié)碼寄存器機(Register Machine)。再如 Mozilla 公司的 SpiderMonkey,他們使用字節(jié)碼的歷史更久,可以追溯到 1998 年的 Netscape 4(見 https://dxr.mozilla.org/class... ),SpiderMonkey 實現(xiàn)的是堆棧機(Stack Machine)。微軟的 Chakra 也使用了字節(jié)碼,他們實現(xiàn)的是寄存器機(Register Machine)。而 v8 之前的做法是比較“脫俗”的,他們跳過了字節(jié)碼這一層,直接把 JS 編譯成機器碼。而在剛剛過去的五一假日前夕,v8 5.9 發(fā)布了,其中的 Ignition 字節(jié)碼解釋器將默認啟動 :https://v8project.blogspot.co... 。v8 自此回到了字節(jié)碼的懷抱。
這讓筆者不禁懷念起 2007 年 Ruby 1.9 的發(fā)布。當時 Ruby 1.9 也是第一次引入了字節(jié)碼,名為 YARV,由笹田耕一領(lǐng)導主導開發(fā)完成。當時,Ruby 還在使用松本行弘的初級的解釋器實現(xiàn),亦即,解釋器每次遍歷代碼的抽象語法樹(AST)來進行 Ruby 代碼的解釋執(zhí)行。而 YARV 則把抽象語法樹(AST)先編譯成字節(jié)碼,然后再運行。引入字節(jié)碼之后,Ruby 的性能得到了顯著的提升。
而這次 V8 引入字節(jié)碼卻是向著相反的方向后退。因為之前 v8 選擇了直接將 JS 代碼編譯到機器代碼執(zhí)行,機器碼的執(zhí)行性能已經(jīng)非常之高,而這次引入字節(jié)碼則是選擇編譯 JS 代碼到一個中間態(tài)的字節(jié)碼,執(zhí)行時是解釋執(zhí)行,性能是低于機器代碼的。最終的性能測試勢必會降低,而不是提高。那么 V8 為什么要做這樣一個退步的選擇呢?為 V8 引入字節(jié)碼的動機又是什么呢?筆者總結(jié)下來有三條:
(主要動機)減輕機器碼占用的內(nèi)存空間,即犧牲時間換空間
提高代碼的啟動速度
對 v8 的代碼進行重構(gòu),降低 v8 的代碼復雜度
故事得從 Chrome 的一個 bug 說起: http://crbug.com/593477 。Bug 的報告人發(fā)現(xiàn),當在 Chrome 51 (canary) 瀏覽器下加載、退出、重新加載 facebook 多次,并打開 about:tracing 里的各項監(jiān)控開關(guān),可以發(fā)現(xiàn)第一次加載時 v8.CompileScript 花費了 165 ms,再次加載加入 V8.ParseLazy 居然依然花費了 376 ms。按說如果 Facebook 網(wǎng)站的 js 腳本沒有變,Chrome 的緩存功能應(yīng)該緩存了對 js 腳本的解析結(jié)果,不該花費這么久。這是為什么呢?
這就是之前 v8 將 JS 代碼編譯成機器碼所帶來的問題。因為機器碼占空間很大,v8 沒有辦法把 Facebook 的所有 js 代碼編譯成機器碼緩存下來,因為這樣不僅緩存占用的內(nèi)存、磁盤空間很大,而且退出 Chrome 再打開時序列化、反序列化緩存所花費的時間也很長,時間、空間成本都接受不了。
所以 v8 退而求其次,只編譯最外層的 js 代碼,也就是下圖這個例子里面綠色的部分。那么內(nèi)部的代碼(如下圖中的黃色、紅色的部分)是什么時候編譯的呢?v8 推遲到第一次被調(diào)用的時候再編譯。這時間上的推移還導致另外一個短板,就是代碼必須被解析多次——綠色的代碼一次、黃色的代碼再解析一次(當 new Person 被調(diào)用)、紅色的代碼再解析一次(當 doWork() 被調(diào)用)。因此,如果你的 js 代碼的閉包套了 n 層,那么最終他們至少會被 v8 解析 n 次。
Facebook 的網(wǎng)站之所以收到這個設(shè)計帶來的負面的性能影響,就是因為他們的前段工程流程中最后把各個獨立的 module 編譯成了一個多帶帶的文件,其中用到了很多閉包,如:
如此一來 Chrome 的緩存作用就只能作用在最外層的 __d() 代碼上,而內(nèi)部的真正的邏輯根本沒有被緩存。
剛才提到了機器碼占空間大的一個壞處,就是不能一次性編譯全部的代碼。機器碼占空間大還有另外一個壞處,就是一些只運行一次的代碼浪費了寶貴的內(nèi)存資源。正如上面 Facebook 中的 __d() 系列函數(shù),他們的作用可能只是注冊、初始化各個模塊組件,而一旦初始化完成便不會再執(zhí)行。但由于機器碼占空間大,這些只執(zhí)行一次的代碼也會在內(nèi)存中長期存在、長期占用空間。正如下圖所示,一般情況下大約 30% 的 V8 堆空間都用來存儲未優(yōu)化的機器碼。
而引入字節(jié)碼之后,占空間的問題就可以得到緩解。通過恰當?shù)卦O(shè)計字節(jié)碼的編碼方式,字節(jié)碼可以做到比機器碼緊湊很多。V8 引入 Ignition 字節(jié)碼后,代碼的內(nèi)存占用確實降低了,如下圖所示。
通過對十大流行手機端網(wǎng)站的測試,可以發(fā)現(xiàn)他們的內(nèi)存占用顯著下降。
這便是 v8 引入字節(jié)碼的主要動機。而這樣實現(xiàn)之后其實順便又帶來了兩個好處,筆者認為可以視作 v8 引入字節(jié)碼的次要動機,亦即:更快的啟動速度和更好的 v8 代碼重構(gòu)。
在啟動速度方面,如今內(nèi)存占用過大的問題消除了,就可以提前編譯所有代碼了。因為前端工程為了節(jié)省網(wǎng)絡(luò)流量,其最終 JS 產(chǎn)品往往不會分發(fā)無用的代碼,所以可以期望全部提前編譯 JS 代碼不會因為編譯了過多代碼而浪費資源。v8 對于 Facebook 這樣的網(wǎng)站就可以選擇全部提前編譯 JS 代碼到字節(jié)碼,并把字節(jié)碼緩存下來,如此 Facebook 第二次打開的時候啟動速度就變快了。下圖是舊的 v8 的執(zhí)行時間的統(tǒng)計數(shù)據(jù),其中 33% 的解析、編譯 JS 腳本的時間在新架構(gòu)中就可以被縮短。
v8 自身的重構(gòu)方面,有了字節(jié)碼,v8 可以朝著簡化的架構(gòu)方向發(fā)展,消除 Cranshaft 這個舊的編譯器,并讓新的 Turbofan 直接從字節(jié)碼來優(yōu)化代碼,并當需要進行反優(yōu)化的時候直接反優(yōu)化到字節(jié)碼,而不需要再考慮 JS 源代碼。最終達到如下圖所示的架構(gòu)。
其實,Ignition + TurboFan 的組合,就是字節(jié)碼解釋器 + JIT 編譯器的黃金組合。這一黃金組合在很多 JS 引擎中都有所使用,例如微軟的 Chakra,它首先解釋執(zhí)行字節(jié)碼,然后觀察執(zhí)行情況,如果發(fā)現(xiàn)熱點代碼,那么后臺的 JIT 就把字節(jié)碼編譯成高效代碼,之后便只執(zhí)行高效代碼而不再解釋執(zhí)行字節(jié)碼。蘋果公司的 SquirrelFish Extreme 也引入了 JIT。SpiderMonkey 更是如此,所有 JS 代碼最初都是被解釋器解釋執(zhí)行的,解釋器同時收集執(zhí)行信息,當它發(fā)現(xiàn)代碼變熱了之后,JaegerMonkey、IonMonkey 等 JIT 便登場,來編譯生成高效的機器碼。
回顧歷史,很多 JS 引擎都是采用了字節(jié)碼這一腳本語言實現(xiàn)技術(shù)的,而 v8 一枝獨秀,走“純機器碼”路線,其實過于激進了:雖然執(zhí)行性能上可以登峰造極,但卻帶來了內(nèi)存占用過大的問題。這次引入字節(jié)碼實則是做了工程上的恰當取舍,將損失掉的內(nèi)存找回來,更加符合如今移動和嵌入式設(shè)備為主的應(yīng)用場景;以時間換空間,讓 v8 能更好的服務(wù)于低內(nèi)存的設(shè)備。如今 V8 也回到了字節(jié)碼的懷抱,不禁令人感嘆 JS 引擎與字節(jié)碼真是有著不解之緣!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/83059.html
摘要:摘要性能彪悍的引擎。深入淺出系列深入淺出第課箭頭函數(shù)中的究竟是什么鬼深入淺出第課函數(shù)是一等公民是什么意思呢深入淺出第課什么是垃圾回收算法深入淺出第課是如何工作的最近,生態(tài)系統(tǒng)又多了個非常硬核的項目。 摘要: 性能彪悍的V8引擎。 《JavaScript深入淺出》系列: JavaScript深入淺出第1課:箭頭函數(shù)中的this究竟是什么鬼? JavaScript深入淺出第2課:函數(shù)是一...
摘要:引擎可以是一個標準的解釋器,也可以是一個將編譯成某種形式的字節(jié)碼的即時編譯器。和其他引擎最主要的差別在于,不會生成任何字節(jié)碼或是中間代碼。不使用中間字節(jié)碼的表示方式,就沒有必要用解釋器了。 原文地址:https://blog.sessionstack.com... showImg(https://segmentfault.com/img/bVVwZ8?w=395&h=395); 數(shù)周之...
摘要:類將源代碼解釋并構(gòu)建成抽象語法樹,使用類來創(chuàng)建它們,并使用類來分配內(nèi)存。類抽象語法樹的訪問者類,主要用來遍歷抽象語法樹。在該函數(shù)中,先使用類來生成抽象語法樹再使用類來生成本地代碼。 通過上一篇文章,我們知道了JavaScript引擎是執(zhí)行JavaScript代碼的程序或解釋器,了解了JavaScript引擎的基本工作原理。我們經(jīng)常聽說的JavaScript引擎就是V8引擎,這篇文章我們...
摘要:最后,客戶端只是依賴于引擎的環(huán)境之一。新的編譯器管道利用來實現(xiàn),并生成可以轉(zhuǎn)換生成器控制流到簡單的本地控制流的字節(jié)碼。可以更容易地優(yōu)化所得到的字節(jié)碼,因為它不需要知道關(guān)于生成器控制流的任何具體內(nèi)容,只是如何保存和恢復函數(shù)的狀態(tài)。 本文轉(zhuǎn)載自:眾成翻譯譯者:smartsrh鏈接:http://www.zcfy.cc/article/2978原文:https://v8project.blo...
摘要:第二篇文章將深入谷歌的引擎的內(nèi)部。引擎可以實現(xiàn)為標準解釋器,或者以某種形式將編譯為字節(jié)碼的即時編譯器。這個引擎是在谷歌中使用的,但是,與其他引擎不同的是也用于流行的。一種更復雜的優(yōu)化編譯器,生成高度優(yōu)化的代碼。不是唯一能夠做到的引擎。 本系列的 第一篇文章 主要介紹引擎、運行時和調(diào)用堆棧。第二篇文章將深入谷歌 V8 的JavaScript引擎的內(nèi)部。 想閱讀更多優(yōu)質(zhì)文章請猛戳GitHu...
閱讀 3538·2021-11-22 15:22
閱讀 3328·2019-08-30 15:54
閱讀 2724·2019-08-30 15:53
閱讀 783·2019-08-29 11:22
閱讀 3529·2019-08-29 11:14
閱讀 2073·2019-08-26 13:46
閱讀 2210·2019-08-26 13:24
閱讀 2277·2019-08-26 12:22