做者 | Mythri Alle, Dan Elphick, Ross McIlroy譯者 | 肖鵬編輯 | Yonie前端
咱們在 2018 年底啓動了一個名爲 V8 Lite 的項目,項目的目標在於大幅下降 V8 引擎的內存使用。這個項目本來被設想爲 V8 的獨立輕量版本,專門爲低內存的移動或嵌入式設備設計。在這種場景中咱們更在意減小內存的使用,而不是執行的吞吐量。不過,在工做進行過程當中咱們意識到,咱們爲 Lite 模式所作的許多內存優化工做能夠被移植到常規版本的 V8 引擎,從而使全部 V8 用戶收益。小程序
這篇文章重點介紹了項目中的關鍵優化策略,以及這些優化如何在實際中節省工做負載內存。前端工程化
輕量模式注意:若是你更喜歡看演講而不是閱讀文章,請欣賞這個視頻: https://youtu.be/56ogP8-eRqA數組
爲了優化 V8 的內存使用,首先須要瞭解 V8 如何使用內存,以及中哪些對象類存在大比例的堆佔用。咱們使用 V8 的內存可視化工具分析了許多典型網頁,追蹤其中堆大小的使用狀況。緩存
V8 引擎加載印度時報網站時不一樣對象類型的堆佔用狀況安全
分析得知,V8 的堆很大一部分被非關鍵對象佔用,這些對象專門用於優化 JavaScript 的執行和異常處理。好比:優化代碼結構;用於肯定如何優化代碼的反饋類型;C++ 和 JavaScript 對象綁定須要的冗餘元數據;堆棧符號化過程當中須要的元數據;只在頁面加載期間執行了幾回函數的字節碼。前端框架
獲得以上結果後,咱們開始研究 V8 的輕量模式。主要策略是:以下降 JavaScript 的執行速度爲代價,大幅減小可選對象的分配量,從而達到節省內存使用的目標。微信
許多輕量模式的改動能夠經過配置現有的 V8 引擎達到目的,好比禁用 TurboFan 優化編譯器。但其餘的改動須要針對 V8 自己進行修改。閉包
值得注意的是,既然輕量模式不須要優化代碼,V8 就能夠再也不收集優化編譯器所需的反饋類型。當 Ignition 解釋器執行代碼時,V8 會收集各類運算(如 + 或 o.foo)中傳遞的操做數的反饋類型,以便在稍後中進行優化。此類信息儲存在反饋向量中,這些向量佔用了堆內存很大一部分。輕量模式能夠避免分配反饋向量,但解釋器與 V8 的內聯緩存基礎架構指望來自於反饋向量的值,所以 V8 須要至關多的代碼重構才能支持移除反饋向量的執行。架構
輕量模式在 v7.3 的 V8 版本中推出。與 v7.1 版本相比,經過禁用代碼優化、禁止分配反饋向量、老化極少執行的代碼(後文中會介紹),新版本在典型網頁場景中減小了 22% 的棧內存使用。對於明確但願以犧牲性能爲代價以得到更好的內存使用率的應用程序而言,這是個好結果。但在完成這項工做的過程當中,咱們意識到能夠經過把 V8 變「lazy」,在不影響性能的狀況下實現輕量模式,以獲得節省內存的結果。
延遲反饋向量分配徹底禁用反饋向量分配不只阻止了 V8 TurboFan 編譯器爲代碼進行優化,還阻止 V8 爲常見的運算作內聯緩存,好比 Ignition 解釋器對於對象屬性的加載。這會形成 V8 的執行時間顯著迴歸,頁面加載時間減小 12%,同時使典型交互式網頁場景中 V8 的 CPU 使用時間增長 120%。
爲了在沒有性能迴歸的狀況下,將大部分節省內存的提高效果帶到常規版本的 V8 版本,咱們採用了一種新的策略:在函數執行了必定量字節碼的代碼(目前爲 1KB)後延遲地分配反饋向量。因爲大多數函數不常常執行,所以延遲策略能夠在大多數狀況下避免反饋向量的分配,但仍然會在須要的地方快速分配它們,讓代碼獲得優化的同時也避免了性能迴歸問題。
這種方法還有一個複雜因素與反饋向量組成的樹有關。在這個樹中,內嵌函數的反饋向量被儲存爲外部函數反饋向量的條目。這是由於新建立的函數閉包須要與同一個函數建立的全部其餘閉包得到相同的反饋向量數組。因爲反饋向量被延遲分配,咱們再也不能使用反饋向量生成此樹,由於沒法保證內部函數分配反饋向量時,外部函數已經分配好了反饋向量。爲了解決這個問題,咱們爲函數建立了新的 ClosureFeedbackCellArray 結構維護這個樹。當函數被調用時,表明它的 ClosureFeedbackCellArray 將會與一個完整的 FeedbackVector 交換。
反饋向量樹在延遲分配先後的比對
實驗結果與實際用戶反饋代表,延遲反饋在桌面端沒有性能迴歸問題。而在移動平臺上,因爲垃圾回收的減小,咱們在某些低端設備中一樣觀察到了性能提高。所以,咱們已經在包含輕量模式的全部 V8 版本中啓用了延遲反饋分配。與最初不作延遲反饋的輕量模式,新方法有一些輕微的內存提高,不過這個代價能夠被實際性能提高補償。
延遲源碼定位在 JavaScript 編譯爲字節碼後,源碼定位表會隨之生成,它記錄了字節碼序列對應的 JavaScript 源碼中字符的位置信息。但源碼定位表只會在符號化異常或者執行如程序調試等開發人員任務時才須要此信息,所以不多會被使用。
爲了不這種浪費,如今 V8 編譯源碼爲字節碼時再也不收集源碼的位置信息(假如沒有附屬調試器或分析器)。源碼位置的收集僅發生在生成堆棧跟蹤時,好比調用 Error.stack 方法,或者將異常堆棧跟蹤打印到控制檯時。這的確有一些成本,由於源碼定位須要從新分析和編譯函數,可是大多數網站並不會在生產中符號化堆棧跟蹤,所以這不會產生任何可感知到的性能影響。
這個策略帶來的一個新的挑戰。咱們須要找到一個生成可重複字節碼的方法,不過這在以前是不作保證的。若是 V8 在收集源碼定位時生成的字節碼與原始代碼不一樣,則產生的定位信息沒法使字節碼與源碼對齊,而且堆棧跟蹤可能指向源碼中錯誤的位置。
某些狀況下,因爲某些解析器信息在函數初始預解析和以後的懶編譯之間丟失,V8 可能會生成不一樣的字節碼,這取決於函數是預編譯仍是懶編譯。這些不匹配大多數是無害的,例如丟失掉不可變變量的跟蹤信息,致使沒法優化這類代碼。但這種不匹配的事實,確實揭露了這項優化某些可能致使代碼錯誤執行的潛在問題。所以,咱們修復了這些不匹配問題,增長了額外的檢查和壓力模式,以確保函數預編譯與懶編譯老是產生一致的輸出,讓咱們對 V8 解析器和預解釋器的正確性和一致性有了更多信心。
字節碼沖刷JavaScript 源碼編譯產生的字節碼會佔用大量 V8 的堆內存,一般佔約 15%,其中包含相關代碼的元數據。許多函數只在初始化階段執行,或者編譯後不多再使用。
所以,咱們增長了一個新特性,假如某個函數最近一段時間沒有執行過,那麼將會在垃圾回收期間將其編譯的字節碼沖刷掉。爲了達成這個目標,咱們爲函數的字節碼增長了老化標記:在每一個主要(mark-compact) 的垃圾回收期間增長老化數值,並在執行函數的時候將數值重置爲零。任何超過老化閾值的字節碼均可能在下一次的垃圾回收期間回收。若是字節碼被回收後函數再次被執行,那麼它將會被從新編譯。
確保字節碼只有真正再也不被須要的時候沖刷是個技術挑戰。例如,若是函數 A 調用了另外一個長時間運行的函數 B,那麼有可能函數 A 仍在調用棧上的時候被增長老化標記。在這種狀況下,即便函數 A 達到了老化閾值咱們也不想沖刷掉函數 A 的字節碼,由於長時間運行的函數 B 在返回的時候須要繼續執行函數 A。所以,字節碼在達到壽命期限時被保留爲弱連接,但若是函數引用存在於調用棧或者其餘地方的時候,其字節碼會保留爲強連接。字節碼在沒有任何強連接的時候纔會被沖刷。
除了沖刷字節碼,V8 同時沖刷了這些函數相關的反饋向量。不過,沖刷字節碼的內存回收週期內無法同時沖刷反饋向量,這是由於它們由同不一樣的對象保持。字節碼由本機上下文獨立的 SharedFunctionInfo 保持,而反饋向量由本機上下文依賴的 JSFunction 保持。所以,反饋向量會在隨後的垃圾回收循環中沖刷。
其餘優化策略圖爲一個老化函數在兩個 GC 循環後的對象佈局
除了上面提到的主要優化點以外,咱們還發現並解決了兩個引起效率低下的問題。
一個是下降了 FunctionTemplateInfo 對象的大小。這些對象儲存了與 FunctionTemplate 有關的內部元數據,使得 Chrome 等 V8 的嵌入程序能夠在 JavaScript 中訪問以 C++ 實現的函數回調。爲了實現 DOM Web API, Chrome 引入了許多 FunctionTemplate 對象,而這些對象會增長 V8 的堆佔用。在分析了 FunctionTemplate 的典型用法後,咱們發現 FunctionTemplateInfo 對象的 11 個字段中,一般只有 3 個字段被修改成非默認值。所以,咱們拆分了 FunctionTemplateInfo 對象,使得不多使用的字段儲存在一個只會按需分配的副表中。
另外一個與如何反優化 TurboFan 產生的優化代碼有關。因爲 TurboFan 會進行推測優化,若是某些優化條件再也不成立,那麼可能就須要回退(反優化)到使用解釋器執行。每一個反優化點都有一個 ID,幫助運行時決定解釋器返回到字節碼哪一個執行位置。優化前,此 ID 經過將優化後的代碼位置跳轉到大型跳轉表的一個特定位置計算,這個跳轉表會載入正確的 ID 到寄存器中,而後跳轉到運行時去處理反優化。這樣作的好處是是優化代碼的每一個反優化點只需一個跳轉指令。然而反優化跳轉表是預先分配的,而且必須足夠大到支持整個反優化 ID 範圍。咱們如今選擇修改 TurboFan 的代碼,使得優化代碼的反優化點在調用運行時以前直接加載反優化 ID。這樣咱們就能夠移除整個大型跳轉表,代價是優化後的代碼大小略有增長。
結果以上是咱們在最近七個版本的 V8 引擎中作的優化。一般來講,這些優化首先引入到輕量模式裏,隨後被帶到 V8 的默認配置。
圖爲一組典型網頁在 AndroidGo 設備上的平均堆大小
圖爲 V8 引擎 v7.8 (Chrome 78) 與 v7.1 (Chrome 7.1) 的單頁內存節省對比差別
在此期間,從一系列經典網站上的結果來看,V8 的堆大小的堆大小平均減少了 18%。這對低端的 AndroidGo 移動設備來講,平均內存佔用減小了 1.5 MB。這些優化沒有對性能基準測試產生重大影響,同時也在實際網頁交互中經受住了考驗。
經過禁用函數優化,輕量模式能夠以 JavaScript 執行吞吐量爲代價進一步節省內存。平均來講,輕量模式能夠爲設備節省 22% 的內存,甚至在有些頁面能夠節省到高達 32%。這對於 AndroidGo 設備來講至關於 1.8 MB 的 V8 堆內存。
圖爲 V8 引擎 v7.8 (Chrome 78) 與 v7.1 (Chrome 7.1) 的內存節省對比差別
將每一個優化點獨立拆分來看時,容易看出不一樣頁面從這些優化中獲得了不一樣比例的優化提高。將來咱們將繼續探索潛在的優化策略,在保證 JavaScript 快速執行的狀況下,進一步下降 V8 內存使用率。
原文:https://v8.dev/blog/v8-lite
活動推薦GMTC 全球大前端技術大會首次落地華南,走入大灣區深圳。
往屆咱們請到了來自 Google、Twitter、Ins、阿里、騰訊、字節跳動、百度、京東、美團等國內外一線科技企業的頂級前端專家,分享了關於小程序、Flutter、Node、RN、前端框架、前端安全、前端工程化、移動 AI 等 50 多個熱門技術專題。
目前大會 7 折最低價售票通道,已經進入倒計時最後一週,詳細請諮詢:13269078023(同微信)。