V8 6.6 進一步改進緩存性能
原文: Improved code caching
作者: Mythri Alle, Chief Code Cacher
在 V8 中,當某些腳本經常使用時,V8 會把這些腳本生產的代碼緩存起來。從 Chrome 66 開始,當引擎在頂層執行後,我們會把生成的更多代碼緩存起來。這會導致初始載入時分析和編譯時間縮短 20-40%。
1. 背景
V8 使用兩種代碼緩存策略來緩存生成的代碼,以便以後重用。
首先是存在於每個 V8 實例中的內存緩存(in-memory cache)。初始編譯後生成的代碼存儲在此緩存中,以源字元串作為 key。這可以在 V8 的相同實例中重複使用。另一種代碼緩存序列化生成的代碼並將其存儲在磁碟上供將來使用。該緩存並不只屬於 V8 的特定實例,可以在 V8 的不同實例中使用。
這篇博文主要關注 Chrome 中使用的第二種代碼緩存。(其他嵌入程序也使用這種代碼緩存;它不僅限於 Chrome,但本博文僅關注 Chrome 中的使用情況。)
Chrome 將序列化的生成代碼(generated code)存儲到磁碟緩存中,並使用腳本資源的 URL 作為 key。載入腳本時,Chrome 會檢查磁碟緩存。如果腳本已被緩存,則 Chrome 會將序列化的數據作為編譯請求的一部分傳遞給 V8。然後 V8 反序列化這些數據,而不是解析和編譯腳本。還有額外的檢查來確保代碼仍然可用(例如:版本不匹配導致緩存的數據無法使用)。
真實世界的數據顯示,代碼緩存命中率(對於可以緩存的腳本)很高(?86%)。雖然這些腳本的緩存命中率很高,但是我們每個腳本緩存的代碼量並不是很高。我們的分析表明,增加緩存的代碼量可以使 JavaScript 代碼的解析和編譯減少大約 40% 的時間。
2. 增加緩存的代碼量
在以前的方法中,代碼緩存與腳本的編譯請求相結合。
嵌入者可以請求 V8 序列化它在頂級編譯新的 JavaScript 源文件時生成的代碼。編譯腳本後,V8 返回序列化代碼。當 Chrome 再次請求相同的腳本時,V8 會從緩存中獲取序列化的代碼並對其進行反序列化。V8 完全避免了重新編譯已經在緩存中的函數。下圖顯示了這些場景:
V8 僅編譯在頂層編譯期間的立即執行的函數(IIFE),並標記用於延遲編譯的其他函數。這樣可以避免編譯不需要的函數,從而提高頁面載入時間,但這也意味著序列化數據僅包含需要迫切編譯的函數的代碼。
在 Chrome 59 之前,我們必須在代碼開始執行之前生成代碼緩存。較早的 V8 基本編譯器(Full-codegen)為執行上下文生成專用代碼。Full-codegen 將代碼補丁用於特定執行上下文的快速路徑(fast-path)操作。當在其他執行上下文中使用的,需要刪除特定於某個上下文的數據,此類代碼不能被輕易地序列化。
隨著在 Chrome 59 中啟用 Ignition,這一限制不再是必要的。Ignition 使用數據驅動的內嵌緩存來執行當前執行上下文中的快速路徑操作。上下文相關數據存儲在反饋向量(feedback vector)中並與生成的代碼分開。通過這種方式也就使得「執行腳本之後也能生成代碼緩存」稱為可能。在我們執行腳本時,會編譯更多的函數(標記為惰性編譯的函數),從而允許我們緩存更多的代碼。
V8 公開了一個新 API,ScriptCompiler::CreateCodeCache
,可以讓代碼緩存請求不再依賴於編譯請求。在編譯請求的過程中請求代碼緩存已被棄用,並且不適用於 V8 v6.6 及更高版本。從版本 66 開始,Chrome 使用此 API 在頂層執行後請求代碼緩存。下圖顯示了請求代碼緩存的新場景。代碼緩存在頂層執行之後被請求,並因此包含在腳本執行期間稍後被編譯的函數的代碼。在後面的運行中(在下圖中顯示為熱運行),它避免了在頂層執行期間編譯函數。
3. 結果
使用我們內部的 real-world benchmarks 測試此功能的性能。下圖顯示了早期高速緩存方案中分析和編譯時間的縮短。在大多數頁面上,解析和編譯時間都會減少 20-40% 左右。
來自其它數據也顯示了和我們相似的結果,在桌面和移動設備上編譯 JavaScript 代碼的時間減少了 20-40%。在 Android 上,這種優化還可以轉化為頂級頁面載入指標減少 1-2%,例如網頁可以被用戶操作時所需的時間。我們還監測了 Chrome 的內存和磁碟使用情況,但沒有看到任何明顯的回歸。
推薦閱讀:
※盤點機床行業人士不得不知的數控加工知識
※IOS11 beta2 今早推送,BUG已修復,所有功能正常使用
※中國腦計劃顛覆性創新之路二,歐美腦計劃存在重大缺陷
※除了「殺熟」,大數據還做了哪些惡
※杭州保姆縱火案、羅一笑事件——2017年這些網路熱議話題你還記得嗎?