跳至主要内容

改進程式碼快取

· 閱讀時間約 5 分鐘
Mythri Alle, 首席程式碼快取專家

V8使用程式碼快取來快取常用腳本的生成程式碼。從Chrome 66開始,我們透過在頂層執行後生成快取,使更多的程式碼得以快取。這使初次載入時的解析及編譯時間減少了20–40%。

背景

V8使用兩種程式碼快取方法來快取生成的程式碼,以便稍後重複使用。第一種是內存快取,在每個V8實例內部可用。初次編譯後生成的程式碼儲存到此快取中,並以來源字串作為鍵值。這些快取可供同一V8實例重複使用。另一種程式碼快取則會將生成的程式碼序列化並儲存到磁碟,以供未來使用。此快取並非特定於某個V8實例,而是可跨不同V8實例使用。此文章主要著重探討第二種快取方式在Chrome中的應用。(其他嵌入者也使用這種程式碼快取方式;此方法並不限於Chrome。然而,本文章僅聚焦於Chrome中的应用。)

Chrome將程式碼的序列化內容儲存到磁碟快取,並以腳本資源的URL作為鍵值。在載入腳本時,Chrome會檢查磁碟快取。如果腳本已經快取,Chrome會將序列化的數據作為編譯請求的一部分傳送給V8。V8隨後反序列化這些數據,而不是重新解析和編譯腳本。也會有額外的檢查步驟,以確保程式碼仍然是可用的(例如:版本不匹配可能導致快取數據不可用)。

實際數據顯示,程式碼快取命中率(對可以快取的腳本而言)較高(~86%)。儘管這些腳本的快取命中率較高,但我們每個腳本快取的程式碼量並不多。我們的分析顯示,增加快取程式碼的量可以將JavaScript程式碼解析和編譯時間減少大約40%。

增加快取的程式碼量

在之前的方法中,程式碼快取與編譯腳本的請求耦合在一起。

嵌入者可以要求V8在新JavaScript源文件頂層編譯期間序列化生成的程式碼。V8在編譯腳本後返回序列化程式碼。當Chrome再次請求相同腳本時,V8從快取中提取序列化程式碼並反序列化它。對於已存在於快取中的函數,V8完全避免重新編譯。下圖展示了這些場景:

V8僅編譯預期會立即執行的函數(IIFEs)進行頂層編譯,並為其它函數標記為延遲編譯。透過避免編譯不必要的函數,提高了頁面加載速度,然而序列化數據僅包含立即編譯的函數程式碼。

在Chrome 59之前,我們必須在任何執行開始之前生成程式碼快取。V8的早期基本編譯器(Full-codegen)針對執行上下文生成專門程式碼。Full-codegen使用程式碼修補以加速特定執行上下文的操作。這類程式碼無法輕易移除上下文特定的數據並序列化,以便在其他執行上下文中使用。

隨著Ignition的亮相於Chrome 59中推出,這項限制已不復存在。Ignition使用數據驅動內聯快取加速當前執行上下文中的操作。上下文相關數據儲存於反饋向量中,與生成的程式碼分開存放。這為於腳本執行後生成程式碼快取提供了可能性。隨著腳本執行,更多被標記為延遲編譯的函數被編譯,使我們得以快取更多的程式碼。

V8 提供了一個新的 API,ScriptCompiler::CreateCodeCache,可在編譯需求之外請求程式碼快取。與編譯請求一起請求程式碼快取的方式已被棄用,並且在 V8 v6.6 之後將無法使用。從版本 66 開始,Chrome 使用此 API 在執行頂層程式後請求程式碼快取。下圖展示了請求程式碼快取的新情境。程式碼快取在頂層執行後請求,因此包含了在腳本執行期間稍後編譯的函數程式碼。在後續操作(如下圖所示的熱執行操作)中,它避免了在頂層執行期間編譯函數的情況。

結果

此功能的性能使用我們內部的真實世界基準測試進行了測量。下圖顯示了與早期快取方案相比,解析和編譯時間的下降。在大多數頁面上,解析和編譯時間分別減少約 20–40%。

來自實際應用的數據顯示了類似的結果,桌面和行動端的 JavaScript 程式碼編譯時間減少了 20–40%。在 Android 平台上,此優化還使頂層頁面載入指標(例如網頁開始互動所需的時間)降低了 1–2%。我們還監測了 Chrome 的記憶體和磁碟使用情況,未發現任何顯著的回歸問題。