?V8 引入全新的非優(yōu)化 JS 編譯器,性能大幅提升!
微信搜索
逆鋒起筆關(guān)注后回復(fù)編程pdf
領(lǐng)取編程大佬們所推薦的 23 種編程資料!


想要編寫高性能的 JavaScript 引擎,光是有高度優(yōu)化的編譯器(如 TurboFan)是不夠的。特別是對于短生命周期的會話(例如加載網(wǎng)站或命令行工具),在高優(yōu)化編譯器開始優(yōu)化之前就已經(jīng)有很多工作要做,更沒有時間去生成什么優(yōu)化代碼了。
正因如此,自 2016 年起,我們不再跟蹤綜合基準測試(如 Octane)的成績,而是轉(zhuǎn)而去衡量實際場景中的性能表現(xiàn)。并且從那時起,我們就一直在努力研究如何提升高優(yōu)化編譯器作用范圍之外的 JavaScript 性能。這意味著我們需要在解析器、流式處理、對象模型、垃圾收集中的并發(fā)性、緩存編譯后的代碼等事項上逐個攻關(guān)……每一個領(lǐng)域都有新鮮的感覺。
當我們轉(zhuǎn)向提升現(xiàn)實場景中初始 JavaScript 的執(zhí)行性能,我們在優(yōu)化解析器時開始遇到諸多局限。V8 的解析器經(jīng)過高度優(yōu)化,速度極快,但解析器總有一些固有開銷是我們無法擺脫的;字節(jié)碼解碼開銷或調(diào)度開銷是解析器功能的內(nèi)在組成部分。
基于我們目前的雙編譯器模式,我們很難更快地升級(tier-up)到優(yōu)化代碼;我們可以(并且正在)提升優(yōu)化的效果,但在某些時候,想要提升速度就只能去掉一些優(yōu)化項,但這會降低峰值性能。更糟糕的是,我們還無法提前優(yōu)化進程,因為我們還沒有穩(wěn)定的對象形態(tài)反饋。
今天我們向大家介紹 Sparkplug:這是我們將隨 V8 v9.1 發(fā)布的,全新的非優(yōu)化 JavaScript 編譯器,位于 Ignition 解析器和 TurboFan 優(yōu)化編譯器之間。

Sparkplug 的設(shè)計目標是快速編譯。非常快,如此之快,讓我們可以隨時隨地進行編譯,于是我們就可以比 TurboFan 代碼更積極地升級到 Sparkplug 代碼。
Sparkplug 編譯器的速度來自于一些技巧。首先,它會作弊;它所編譯的函數(shù)已經(jīng)被編譯為字節(jié)碼,并且字節(jié)碼編譯器已經(jīng)完成了大多數(shù)艱苦的工作,例如變量解析、弄清楚括號是否實際上是箭頭函數(shù)、消除結(jié)構(gòu)化語句等等。Sparkplug 從字節(jié)碼而不是 JavaScript 源代碼進行編譯,因此不必操心這些麻煩的事情。
第二招是,Sparkplug 不會像大多數(shù)編譯器那樣生成任何中間表示(IR)。相反,Sparkplug 通過字節(jié)碼的一次線性 pass 直接編譯為機器碼,并發(fā)出與該字節(jié)碼的執(zhí)行相匹配的代碼。實際上,整個編譯器是一個 for 循環(huán)內(nèi)的一個 switch 語句,分派給固定的,按字節(jié)碼的機器碼生成函數(shù)。
for (; !iterator.done(); iterator.Advance()) {VisitSingleBytecode()}
缺少 IR 意味著編譯器的優(yōu)化機會有限,只能做一些非常本地的小幅度優(yōu)化。這也意味著我們必須將整個實現(xiàn)分別移植到我們支持的每種架構(gòu)上,因為這里沒有架構(gòu)無關(guān)的中間階段。但事實證明這些都不是問題:快速編譯器是簡單編譯器,因此代碼很容易移植;并且 Sparkplug 不需要大量優(yōu)化,因為我們稍后會在管道中提供優(yōu)化效果很出色的編譯器。
從技術(shù)上講,我們目前對字節(jié)碼進行了兩次 pass——一次用來發(fā)現(xiàn)循環(huán),第二次生成實際代碼。不過,我們最終的計劃是擺脫第一個。
向現(xiàn)有的成熟 JavaScript VM 添加新的編譯器是一項艱巨的任務(wù)。除了標準執(zhí)行之外,你還需要支持各種各樣的事情;V8 有一個調(diào)試器、一個 stack-walking CPU profiler、針對異常的堆棧跟蹤、集成到升級、堆棧替換以優(yōu)化代碼實現(xiàn)熱循環(huán)……實在很多。
Sparkplug 巧妙地簡化了所有這些問題,具體方法就是保持一個“與解析器兼容的堆棧框架”。
稍微解釋下。堆棧框架(Stack frame)是代碼執(zhí)行存儲函數(shù)狀態(tài)的方式。每當你調(diào)用一個新函數(shù)時,它都會為該函數(shù)的局部變量創(chuàng)建一個新的堆棧框架。一個堆棧框架由一個框架指針(標記其開始)和一個堆棧指針(標記其結(jié)束)定義:

看到這里,很多讀者會表示抗議:“這張圖不對啊,堆棧明顯是朝著相反的方向的!”。別急,我為你做了一個按鈕:
當一個函數(shù)被調(diào)用時,返回地址被推入這個堆棧;該函數(shù)返回時會彈出它,來知道該返回到何處。然后,當該函數(shù)創(chuàng)建一個新框架時,它將舊的框架指針保存在堆棧上,并將新的框架指針設(shè)置為指向它自己的堆棧框架的起始。因此,這個堆棧有了一個框架指針鏈,每個框架指針都指向前一個框架的起始:

嚴格來說這只是一個約定,后面是生成的代碼,它不是必需的。不過這是一種相當常見的方式;唯一真正中斷的一次是堆棧框架完全清除的時候,或者可以改用調(diào)試邊表(side-table)遍歷堆棧框架的時候。
這是針對所有函數(shù)類型的常規(guī)堆棧布局;然后是關(guān)于如何傳遞參數(shù),以及函數(shù)如何在其框架中存儲值的約定。在 V8 中,我們有針對 JavaScript 框架的約定,即在調(diào)用函數(shù)之前將參數(shù)(包括接收器)以相反的順序推入堆棧,并且堆棧上的前幾個槽為:被調(diào)用的當前函數(shù);被調(diào)用的上下文;以及傳遞的參數(shù)數(shù)量。這是我們的“標準”JS 框架布局:

這個 JS 調(diào)用約定在優(yōu)化框架和解析框架之間共享,這樣一來,當我們在調(diào)試器的性能面板中調(diào)優(yōu)代碼時,就能以最小的開銷遍歷堆棧,諸如此類。
對于 Ignition 解析器來說,約定變得更加顯式。Ignition 是基于寄存器的解析器,這意味著存在一些虛擬寄存器(請勿與機器寄存器混淆!)來存儲解析器的當前狀態(tài)——其中包括 JavaScript 函數(shù)的本地變量(var/let/const 聲明)和臨時值。這些寄存器與要執(zhí)行的字節(jié)碼數(shù)組指針,以及該數(shù)組中當前字節(jié)碼的偏移量一起存儲在解析器的堆棧框架中:

Sparkplug 會有意創(chuàng)建并維護一個與解析器的框架相匹配的框架布局;只要解析器存儲一個寄存器值,Sparkplug 也會存儲一個值。這樣做有幾個原因:
它簡化了 Sparkplug 的編譯過程;Sparkplug 可以只鏡像解析器的行為,而無需保留從解析器寄存器到 Sparkplug 狀態(tài)的某種映射。 由于字節(jié)碼編譯器完成了分配寄存器的重活兒,因此它還加快了編譯速度。 它大大簡化了與系統(tǒng)其余部分的集成工作。調(diào)試器、profiler、異常堆棧展開、堆棧跟蹤打印,所有這些操作都會執(zhí)行堆棧遍歷以發(fā)現(xiàn)當前正在執(zhí)行的函數(shù)堆棧,并且所有這些操作都不需要做什么更改就能繼續(xù)搭配 Sparkplug,因為就它們而言,它們有的只是一個解析器框架。 它簡化了堆棧替換(OSR)。OSR 是指在執(zhí)行過程中替換當前正在執(zhí)行的函數(shù);當前,當一個已解析函數(shù)在一個熱循環(huán)內(nèi)(在該循環(huán)中它升級為優(yōu)化代碼),以及在優(yōu)化代碼取消優(yōu)化(在其降級并繼續(xù)在解析器中執(zhí)行該函數(shù))時,就會發(fā)生這種情況。使用 Sparkplug 框架鏡像解析器框架時,任何適用于解析器的 OSR 邏輯都將適用于 Sparkplug;更棒的是,我們可以在解析器和 Sparkplug 代碼之間切換,而框架轉(zhuǎn)換開銷幾乎為零。
我們對解析器堆棧框架做了一個小更改,即在 Sparkplug 代碼執(zhí)行期間,我們不讓字節(jié)碼偏移保持最新。相反,我們存儲一個從 Sparkplug 代碼地址范圍到對應(yīng)的字節(jié)碼偏移量的雙向映射。這是一種相對簡單的編碼映射,因為 Sparkplug 代碼是直接從字節(jié)碼上的一個線性遍歷發(fā)出的。每當一個堆棧框架訪問想要知道一個 Sparkplug 框架的“字節(jié)碼偏移量”時,我們都會在此映射中查找當前執(zhí)行的指令,并返回相應(yīng)的字節(jié)碼偏移量。類似地,每當我們想將 OSR 從解析器轉(zhuǎn)換為 Sparkplug 時,我們都可以在映射中查找當前字節(jié)碼偏移量,然后跳轉(zhuǎn)到相應(yīng)的 Sparkplug 指令。
你可能會注意到,我們現(xiàn)在在堆棧框架上有一個未使用的插槽,字節(jié)碼偏移量就會在這個插槽上。由于我們希望保持堆棧的其余部分不變,因此我們不能放棄它。我們重新調(diào)整了這個堆棧插槽的功能,讓它為當前正在執(zhí)行的函數(shù)緩存“反饋向量”。這是用于存儲對象形態(tài)數(shù)據(jù)的向量,大多數(shù)操作都需要加載它。我們要做的只是謹慎一點對待 OSR,確保我們?yōu)檫@個插槽要么換入正確的字節(jié)碼偏移量,要么換入正確的反饋向量。
于是 Sparkplug 堆棧框架為:

一個 V8 Sparkplug 堆棧框架
實際上,Sparkplug 很少生成自己的代碼。JavaScript 語義很復(fù)雜,即使執(zhí)行最簡單的操作也需要大量代碼。由于多種原因,強制 Sparkplug 在每次編譯時內(nèi)聯(lián)重新生成這些代碼都是不好的:
由于需要生成大量代碼,這將明顯增加編譯時間, 這會增加 Sparkplug 代碼的內(nèi)存消耗,并且 我們必須重新實現(xiàn)用于 Sparkplug 的一堆 JavaScript 功能的代碼源,這可能意味著會有更多的錯誤和更大的受攻擊面。
因此,大多數(shù) Sparkplug 代碼只是調(diào)用“內(nèi)置代碼”,即嵌入二進制文件中的小段機器碼片段,以完成那些臟活兒。這些內(nèi)置代碼要么就是解析器用的那些,或者至少與解析器的字節(jié)碼處理程序共享大部分代碼。
實際上,Sparkplug 代碼基本上只是內(nèi)置代碼的調(diào)用和控制流:
你現(xiàn)在可能會想,“那么,這一切到底有什么意義?Sparkplug 不是在做與解析器相同的工作嗎?”——你的疑問是有道理的。在許多方面,Sparkplug 只是解析器執(zhí)行的一個序列化,它調(diào)用相同的內(nèi)置函數(shù)并維護相同的堆棧框架。但這樣做也是值得的,因為它消除(或更準確地說是預(yù)編譯)了那些不可移動的解析器開銷,例如操作數(shù)解碼和下一個字節(jié)碼分派。
事實證明,解析器破壞了許多 CPU 優(yōu)化工作:解析器從內(nèi)存中動態(tài)讀取靜態(tài)操作數(shù),從而迫使 CPU 停頓或推測值可能是多少。分派到下一個字節(jié)碼需要成功的分支預(yù)測才能保持高性能,即使推測和預(yù)測正確,你還是要執(zhí)行所有解碼和分派代碼,并且你還是會在各個緩沖區(qū)和緩存中浪費寶貴的空間。CPU 實際上本身就是一個解析器,只不過它是機器碼的解析器。這樣看來,Sparkplug 是從 Ignition 字節(jié)碼到 CPU 字節(jié)碼的一個“轉(zhuǎn)譯器”,將你的函數(shù)從在“仿真器”中運行移到了“原生”運行。
那么,Sparkplug 在現(xiàn)實場景中的性能表現(xiàn)如何呢?我們用 Chrome M91 跑了一些基準測試,用了幾個性能 bot,分別啟用和關(guān)閉 Sparkplug 來觀察其影響。
劇透:我們非常滿意。
以下基準測試列出了運行多個操作系統(tǒng)的 bot。雖說系統(tǒng)和 bot 的名字差不多,但我們認為它并不會對結(jié)果產(chǎn)生太大影響。另外,不同的機器也有不同的 CPU 和內(nèi)存配置,我們認為這是差異的主要來源。
Speedometer 是一個基準測試,它使用一些流行的框架構(gòu)建一個 TODO 列表跟蹤 Web 應(yīng)用程序,并通過添加和刪除 TODO 對應(yīng)用程序進行性能壓力測試,來模擬現(xiàn)實世界中網(wǎng)站框架的使用情況。我們發(fā)現(xiàn)它很好地反映了現(xiàn)實世界中的負載和互動行為,并且我們屢屢發(fā)現(xiàn),Speedometer 的成績提升反映在了我們的現(xiàn)實世界指標中。
使用 Sparkplug,Speedometer 得分提高 5-10%,具體取決于我們觀察的 bot。

使用 Sparkplug 在多個性能 bot 中改善了 Speedometer 的得分中位數(shù)。誤差線表示四分位間距。
Speedometer 是一個很好的基準測試,但它只反映了部分情況。此外,我們還有一組“瀏覽基準測試”,它們記錄了一組真實的網(wǎng)站,我們可以重播這些內(nèi)容、編寫一些交互腳本,并更真實地了解我們的各種指標在現(xiàn)實世界中的表現(xiàn)。
在這些基準測試上,我們選擇查看“V8 主線程時間”指標,其測試主線程(不包括流解析或后臺優(yōu)化的編譯)在 V8 中花費的總時間(包括編譯和執(zhí)行)。這是在不排除其他基準噪聲源的情況下查看 Sparkplug 自身回報的最佳方法。
結(jié)果各不相同,并且完全取決于機器和網(wǎng)站,但總體而言它們看起來不錯:我們看到大約有 5-15%的改進。

在我們的瀏覽基準測試中,V8 主線程時間得到了 10 個百分點的中位數(shù)改進。誤差線表示四分位間距。
結(jié)論:V8 有了全新的超快速非優(yōu)化編譯器,可將 V8 在實際基準測試中的性能提高 5-15%。V8 v9.1 中已經(jīng)在 --sparkplug 標志后面提供了這一工具,并且隨著 M91 的發(fā)布,我們將在 Chrome 中推出該編譯器。
逆鋒起筆是一個專注于程序員圈子的技術(shù)平臺,你可以收獲最新技術(shù)動態(tài)、最新內(nèi)測資格、BAT等大廠大佬的經(jīng)驗、增長自身、學習資料、職業(yè)路線、賺錢思維,微信搜索逆鋒起筆關(guān)注!
Docker 鏡像優(yōu)化:從 1.16GB 到 22.4MB
支持下

