做者:Mythri Alle, Dan Elphick, and Ross McIlroy翻譯:瘋狂的技術宅前端
原文:https://v8.dev/blog/v8-lite程序員
未經容許嚴禁轉載面試
在 2018 年底,爲了大幅減小 V8 的內存使用量,咱們啓動了一個名爲 V8 Lite 的項目。該項目最初被設想爲 V8 的一個獨立的 精簡模式(Lite mode),專門針對低內存移動設備或嵌入式用例,這些用例更關心的是減小內存的使用而不是吞吐量的執行速度。可是在進行這項工做的過程當中,咱們意識到爲Lite 模式所作的許多內存優化均可以轉移到常規 V8 中,從而使 V8 的全部用戶受益。segmentfault
本文重點介紹了咱們開發的一些關鍵優化以及它們在實際工做負載中對內存所作的優化。數組
注意:若是您不喜歡閱讀文章,請欣賞下面的視頻!瀏覽器
Ross McIlroy在BlinkOn 10上發表的 「V8 Lite – 減小 JavaScript 內存」。緩存
https://www.youtube.com/embed...服務器
爲了優化 V8 的內存使用,咱們首先須要瞭解 V8 如何使用內存以及哪些對象類型在 V8 堆中佔了很大的比例。咱們用了 V8 的內存可視化工具來跟蹤許多典型網頁的堆內容的構成。微信
加載印度時報時,不一樣對象類型所使用的 V8 堆的百分比多線程
爲此,咱們肯定了對 JavaScript 執行並非必不可少的對象在 V8 堆中佔了很大一部分 ,可是這些對象被用於優化 JavaScript 執行,並處理特殊狀況。例如:優化的代碼;類型反饋,用於肯定如何優化代碼;用於在 C++ 和 JavaScript 對象之間進行綁定的冗餘元數據;僅在特殊狀況下才須要元數據,如堆棧跟蹤符號;還有在頁面加載期間僅執行幾回的函數的字節碼。
結果,咱們開始在 V8 的 精簡模式 上進行工做,該模式經過大幅減小這些可選對象的分配來權衡 JavaScript 執行的速度與節省的內存。
經過配置現有的 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
。
惰性反饋分配先後的反饋矢量樹
咱們實驗和現場測試結果代表,在臺式機上的惰性反饋沒有出現性能降低的趨勢,而在移動平臺上,因爲減小了垃圾收集,實際上在低端設備上性能有所提升。所以咱們在全部 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 週期中刷新反饋向量。
通過兩個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 的默認配置。
AndroidGo設備上一組典型網頁的 V8 堆的平均大小
與v7.1(Chrome 71)相比,V8 的 v7.8(Chrome 78)版本每種頁面的內存節省狀況詳情
在這段時間裏,咱們在一系列典型網站上將 V8 堆大小平均減小了 18%,這對應於低端 AndroidGo 移動設備,平均減小了 1.5 MB。在基準測試或實際的網頁交互中,這對 JavaScript 性能可能並無什麼重大影響。
精簡模式能夠經過禁用函數優化來進一步節省內存,但會以必定的成本提升 JavaScript 執行吞吐量。平均而言,精簡模式可節省 22% 的內存,而某些頁面最多可節省 32%。這對應於 AndroidGo 設備上的 V8 堆大小減小了 1.8 MB。
與 v7.1(Chrome 71)相比,V8 v7.8(Chrome 78)的內存用量減小了
當把每一個優化的影響分開來看時,很明顯,不一樣的頁面會從每個優化中得到不一樣比例的收益。展望將來,咱們將繼續尋找潛在的優化方案,這些優化方案能夠進一步減小 V8 對內存的使用量,同時仍然保持 JavaScript 驚人的執行速度。