更輕量級的 V8 引擎

作者:Mythri Alle, Dan Elphick, and Ross McIlroy

翻譯:瘋狂的技術宅

原文:https://v8.dev/blog/v8-lite

未經允許嚴禁轉載

在 2018 年末,爲了大幅減少 V8 的內存使用量,我們啓動了一個名爲 V8 Lite 的項目。該項目最初被設想爲 V8 的一個獨立的 精簡模式(Lite mode),專門針對低內存移動設備或嵌入式用例,這些用例更關心的是減少內存的使用而不是吞吐量的執行速度。但是在進行這項工作的過程中,我們意識到爲Lite 模式所做的許多內存優化都可以轉移到常規 V8 中,從而使 V8 的所有用戶受益。

本文重點介紹了我們開發的一些關鍵優化以及它們在實際工作負載中對內存所做的優化。

注意:如果您不喜歡閱讀文章,請欣賞下面的視頻!

Ross McIlroy在BlinkOn 10上發表的 “V8 Lite⁠ – 減少 JavaScript 內存”

https://www.youtube.com/embed...

Lite 模式

爲了優化 V8 的內存使用,我們首先需要了解 V8 如何使用內存以及哪些對象類型在 V8 堆中佔了很大的比例。我們用了 V8 的內存可視化工具來跟蹤許多典型網頁的堆內容的構成。

clipboard.png

加載印度時報時,不同對象類型所使用的 V8 堆的百分比

爲此,我們確定了對 JavaScript 執行並不是必不可少的對象在 V8 堆中佔了很大一部分 ,但是這些對象被用於優化 JavaScript 執行,並處理特殊情況。例如:優化的代碼;類型反饋,用於確定如何優化代碼;用於在 C++ 和 JavaScript 對象之間進行綁定的冗餘元數據;僅在特殊情況下才需要元數據,如堆棧跟蹤符號;還有在頁面加載期間僅執行幾次的函數的字節碼。

結果,我們開始在 V8 的 精簡模式 上進行工作,該模式通過大幅減少這些可選對象的分配來權衡 JavaScript 執行的速度與節省的內存。

clipboard.png

通過配置現有的 V8 設置,可以對精簡模式進行許多更改,例如禁用 V8 的 TurboFan 優化編譯器。但是其他的優化還需要對 V8 進行更多的修改。

特別是,由於我們決定在精簡模式下無法優化代碼,因此可以避免收集優化編譯器所需的類型反饋。在 Ignition 解釋器中執行代碼時,V8 會收集有關傳遞給各種操作的操作數類型(例如,+o.foo)的反饋,以便針對這些類型調整以後的優化。這些信息存儲在反饋向量中,這些向量在 V8 堆內存中使用了很大的一部分。 精簡模式可以避免分配這些反饋向量,但是 V8 的解釋器和部分內聯緩存基礎結構卻希望反饋向量可用,因此還需要進行大量重構才能支持這種無反饋執行。

在 V8 的 v7.3 版本中啓動的精簡模式與 v7.1 相比,通過禁用代碼優化,不分配反饋矢量以及執行很少執行的字節碼老化(如下所述),使典型的網頁堆大小減少了 22%。對於那些明顯想要權衡性能以提高內存使用率的程序而言,這是一個非常不錯的結果。但是在執行此項工作的過程中,我們意識到通過使 V8 變得更懶惰,可以實現節省精簡模式的大部分內存,而不會影響性能。

惰性反饋分配

完全禁用反饋向量分配,不僅會阻止 V8 的 TurboFan 編譯器對代碼進行優化,而且還會阻止 V8 執行常見操作(例如對象)的 inline caching 屬性在 Ignition 解釋器中的加載。所以這樣做會大大降低 V8 的執行時間,在典型的交互式網頁方案中,頁面加載時間減少了 12%,而 V8 使用的 CPU 時間增加了120%。

爲了在不進行這些迴歸的情況下將節省的大部分內存用於常規 V8,我們轉而採用了另一種方法,在該函數執行了一定數量的字節碼(當前爲1KB)之後,開始惰性分配反饋向量。由於大多數函數並不是要經常執行,因此在大多數情況下,我們避免分配反饋矢量,而是在需要的地方快速分配它們,以避免性能下降,並且仍然可以對代碼進行優化。

這種方法的另一個複雜性與以下事實有關:反饋向量形成一棵樹,內部函數的反饋向量被保留爲外部函數的反饋向量中的條目。這是非常必要的,這樣可以使新創建的函數閉包與爲同一函數創建的所有閉包一樣,接收相同的反饋矢量數組。在惰性分配反饋向量的情況下,我們無法用反饋向量來形成這棵樹,因爲無法保證外部函數會在內部函數分配其反饋向量之前就對其進行分配。爲了解決這個問題,我們創建了一個新的 ClosureFeedbackCellArray 來維護這棵樹,然後在函數變熱時用一個完整的 FeedbackVector 換出一個函數的 ClosureFeedbackCellArray

clipboard.png

惰性反饋分配前後的反饋矢量樹

我們實驗和現場測試結果表明,在臺式機上的惰性反饋沒有出現性能下降的趨勢,而在移動平臺上,由於減少了垃圾收集,實際上在低端設備上性能有所提高。因此我們在所有 V8 版本中都啓用了惰性反饋分配,其中包括精簡模式,與我們原始的無反饋分配方法相比,內存模式略有退步,但是實際性能卻得到了很大的提高。

惰性源位置

從 JavaScript 編譯字節碼時,會生成把字節碼序列與 JavaScript 源碼中的字符位置相關聯的源位置表。但是僅在符號化異常或執行開發人員任務(例如調試)時才需要此信息,因此很少使用。

爲了避免這種浪費,現在編譯字節碼時不收集源位置(假設未連接調試器或分析器),僅在實際生成堆棧跟蹤時(例如,在調用 Error.stack 或將異常的棧跟蹤打印到控制檯時)才收集源。這確實需要付出一些代價,因爲生成源位置需要重新解析和編譯函數,但是大多數網站並未在生產中使用棧跟蹤符號,所以看不到什麼能夠觀察到的性能影響。

我們必須解決的一個問題是需要可重複的字節碼生成,而這是以前無法保證的。如果 V8 在收集源位置時與原始代碼生成不同的字節碼,則源位置不對齊,並且堆棧跟蹤可能指向源代碼中的錯誤位置。

在某些情況下,由於在函數在先急速解析再延遲編譯時丟失了一些解析信息,V8 可能會根據某個函數是急速還是延遲編譯來生成不同的字節碼。這些不匹配大多是良性的,例如,忘記了變量是不可變的事實,因此無法對其進行優化。但是,這項工作發現的某些不匹配在某些情況下確實有可能導致代碼錯誤的執行。因此,我們修復了這些不匹配問題,並添加了檢查和壓力模式,以確保函數的急速和惰性編譯始終能夠產生一致的輸出,從而使我們對 V8 解析器和預解析器的正確性和一致性更具信心。

字節碼刷新

從 JavaScript 源碼編譯的字節碼佔據了 V8 堆空間的很大一部分,通常大約爲 15%,其中包括相關的元數據。有許多函數僅在初始化的時候執行,或者在編譯後很少被使用。

所以我們添加了對垃圾回收期間從函數中清除編譯後的字節碼的支持,如果它們最近沒有執行過的話。爲此我們要跟蹤函數字節碼的 age,增加每個 major(mark-compact)垃圾回收的 age,並在執行該函數時將其重置爲零。任何超過老化閾值的字節碼都可以在下一次垃圾回收中被收集。如果已收集了,但是稍後需要再次執行,那麼將會重新編譯它。

要確保只在不再需要字節碼時才刷新它存在着技術難題。如果函數 A 調用另一個長期運行的函數 B,則函數 A 可能會在其仍在堆棧中時老化。即使函數 A 達到了老化閾值我們也不希望刷新它的字節碼,因爲我們需要在長時間運行的函數 B 返回到 A。因此當字節碼達到函數的老化閾值時,我們會將其視爲函數的弱保留,而堆棧或其他位置對它的任何引用都作爲強保留。我們僅在沒有強鏈接剩餘時才刷新代碼。

除了刷新字節碼,我們還刷新與這些刷新函數關聯的反饋向量,但是我們無法在與字節碼相同的 GC 週期內刷新它們,因爲它們沒有被同一對象保留。字節碼由與本機上下文無關的 SharedFunctionInfo 保留,而反饋向量則由依賴於本機上下文的 JSFunction 保留。最後我們在隨後的 GC 週期中刷新反饋向量。

clipboard.png

經過兩個GC循環後,老化的函數的對象佈局

其他優化

除了這些較大的項目,我們還發現並解決了一些導致效率低下的問題。

第一個是減小 FunctionTemplateInfo 對象的大小。這些對象存儲與 FunctionTemplate 有關的內部元數據,這些元數據用於使嵌入程序(例如 Chrome)提供可被調用的函數的 C++ 回調實現。通過 JavaScript 代碼。 Chrome 瀏覽器引入了許多 FunctionTemplates 以實現 DOM Web API,因此,FunctionTemplateInfo 對象對 V8 的堆大小有所貢獻。在分析 FunctionTemplates 的典型用法之後,我們發現在 FunctionTemplateInfo 對象上的11個字段中,通常只有 3 個被設置爲非默認值。因此我們拆分了 FunctionTemplateInfo 對象,以便將稀有字段存儲在邊表中,該邊表僅在需要時才按需分配。

第二個優化與如何取消 TurboFan 的代碼優化有關。由於 TurboFan 執行推測性優化,所以如果某些條件不再成立,則可能需要回退到解釋器(取消優化)。每個取消點都有一個 ID,該 ID 可以使運行時能夠確定字節碼應該把執行返回到解釋器中的哪個位置上。以前通過優化代碼跳轉到大型跳轉表中的特定偏移量來計算這個 ID,然後再將正確的 ID 加載到寄存器中,最後跳轉到運行時以執行反優化。這樣做的好處是,對於每個取消點,在優化代碼中只需要一條跳轉指令。但是,取消優化跳轉表已經預先分配,並且它必須足夠大,這樣才能支持整個取消優化 id 的範圍。所以我們修改了 TurboFan,使優化代碼中的 deopt 點在調用運行時之前可以直接加載 deopt id。這樣我們就能夠完全刪除這個大型跳轉表,但是代價是需要略微增加優化代碼的大小。

結果

我們已經在 V8 最後七個版本中發佈了上述優化。通常,它們首先以精簡模式開始,然後又被帶到 V8 的默認配置。

clipboard.png

AndroidGo設備上一組典型網頁的 V8 堆的平均大小

clipboard.png

與v7.1(Chrome 71)相比,V8 的 v7.8(Chrome 78)版本每種頁面的內存節省情況詳情

在這段時間裏,我們在一系列典型網站上將 V8 堆大小平均減少了 18%,這對應於低端 AndroidGo 移動設備,平均減少了 1.5 MB。在基準測試或實際的網頁交互中,這對 JavaScript 性能可能並沒有什麼重大影響。

精簡模式可以通過禁用函數優化來進一步節省內存,但會以一定的成本提高 JavaScript 執行吞吐量。平均而言,精簡模式可節省 22% 的內存,而某些頁面最多可節省 32%。這對應於 AndroidGo 設備上的 V8 堆大小減少了 1.8 MB。

clipboard.png

與 v7.1(Chrome 71)相比,V8 v7.8(Chrome 78)的內存用量減少了

當把每個優化的影響分開來看時,很明顯,不同的頁面會從每一個優化中獲得不同比例的收益。展望未來,我們將繼續尋找潛在的優化方案,這些優化方案可以進一步減少 V8 對內存的使用量,同時仍然保持 JavaScript 驚人的執行速度。


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,每天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,每天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章