產品新訊

編譯速度提升 18%,效能絲毫不打折

8 分鐘小故事

Android 執行階段 (ART) 團隊將編譯時間縮短了 18%,同時確保編譯後的程式碼品質,以及記憶體用量不會大幅增加。這項改善措施是 2025 年計畫的一部分,旨在縮短編譯時間,同時維持記憶體用量和編譯後的程式碼品質。

最佳化編譯時間速度對 ART 至關重要。舉例來說,即時 (JIT) 編譯會直接影響應用程式效率和整體裝置效能。編譯速度越快,最佳化作業啟動前的時間就越短,使用者體驗也就越流暢且反應越快。此外,無論是 JIT 或預先 (AOT) 編譯,編譯時間速度的提升都會減少編譯過程中的資源消耗,進而延長電池續航力並降低裝置溫度,尤其是在低階裝置上更是如此。

我們已在 2025 年 6 月的 Android 版本中推出部分編譯時間速度改善功能,其餘功能則會在年底的 Android 版本中推出。此外,搭載 Android 12 以上版本的所有使用者,都能透過主線更新取得這些改善項目。

最佳化最佳化編譯器

最佳化編譯器時,一律要做出取捨。您無法免費獲得速度,必須有所犧牲。我們為自己設定了非常明確且具挑戰性的目標:加快編譯器速度,但不得造成記憶體回歸,且最重要的是,不得降低編譯器產生的程式碼品質。如果編譯器速度變快,但應用程式執行速度變慢,我們就失敗了。

我們願意投入的唯一資源是自己的開發時間,深入研究並找出符合這些嚴格條件的巧妙解決方案。讓我們進一步瞭解我們如何找出需要改善的部分,以及針對各種問題找到合適的解決方案。

尋找值得採用的最佳化建議

開始最佳化指標前,您必須先能夠評估指標。否則,您永遠無法確定是否有所改善。幸運的是,只要採取一些預防措施,例如在變更前後使用同一部裝置進行評估,並確保裝置不會因過熱而降低效能,編譯時間速度就會相當穩定。此外,我們也有編譯器統計資料等決定性評估指標,可協助瞭解幕後運作情況。

 

由於我們為這些改善項目犧牲的資源是開發時間,因此我們希望盡快完成疊代。這表示我們抓取了少數代表性應用程式 (包括第一方應用程式、第三方應用程式和 Android 作業系統本身),以製作解決方案原型。稍後,我們透過大規模的手動和自動測試,確認最終實作項目是否值得。

 

有了這組精心挑選的 APK,我們就能在本機觸發手動編譯、取得編譯設定檔,並使用 pprof 視覺化呈現時間花費在哪裡。

image.png

pprof 中剖析檔的火焰圖示例

pprof 工具功能強大,可讓我們對資料進行切片、篩選及排序,例如查看哪些編譯器階段或方法耗費最多時間。我們不會詳細說明 pprof 本身,只要知道如果長條較長,就表示編譯時間較長。

其中一種是「由下而上」的檢視畫面,可顯示哪些方法耗費最多時間。在下圖中,我們可以看到名為「Kill」的方法,占編譯時間的 1% 以上。網誌文章稍後也會討論其他熱門方法。

image.png

從底部往上看的個人資料

在最佳化編譯器中,有一個階段稱為「全域值編號」(GVN)。您不必擔心整體運作方式,但相關部分是瞭解它有一個名為「Kill」的方法,會根據篩選器刪除某些節點。這很耗時,因為必須逐一檢查所有節點。我們發現,在某些情況下,無論當時存活的節點為何,我們都能預先知道檢查結果為 false。在這些情況下,我們可以完全略過疊代,將疊代次數從 1.023% 降至約 0.3%,並將 GVN 的執行階段縮短約 15%。

導入值得採用的最佳化措施

我們已介紹如何測量及偵測時間的耗用位置,但這只是開始。下一步是瞭解如何最佳化編譯時間。

通常在上述 `Kill` 的案例中,我們會查看節點的疊代方式,並透過平行處理或改善演算法本身等方式加快速度。事實上,我們一開始就是這麼做的,但當我們找不到任何可執行的動作時,突然靈機一動,發現解決方案是 (在某些情況下) 完全不疊代!進行這類最佳化時,很容易見樹不見林。

在其他情況下,我們使用了多種不同技術,包括:

  • 使用經驗法則判斷最佳化作業是否無法產生有價值的結果,因此可以略過
  • 使用額外資料結構來快取運算資料
  • 變更目前的資料結構,以提升速度
  • 延遲計算結果,避免在某些情況下出現週期
  • 使用正確的抽象概念 - 不必要的功能可能會導致程式碼變慢
  • 避免在多次載入時追蹤常用指標

如何判斷最佳化是否值得進行?

這就是有趣的地方,您不必這麼做。偵測到某個區域耗用大量編譯時間,並投入開發時間嘗試改善後,有時您就是找不到解決方案。也許沒什麼可做的、實作時間太長、會大幅降低其他指標、增加程式碼集複雜度等。請注意,本文中每個成功的最佳化案例背後,都有無數個未實現的案例。

如果遇到類似情況,請盡量減少工作量,估算指標的改善幅度。也就是說,請依序執行下列操作:

  1. 根據您已收集的指標或直覺估算
  2. 使用快速原型估算
  3. 導入解決方案。

別忘了估算解決方案的缺點。舉例來說,如果您要依賴額外的資料結構,願意使用多少記憶體?

深入探索

廢話不多說,現在就來看看我們實施的幾項變更。

我們變更了名為 FindReferenceInfoOf 的方法,藉此進行最佳化。這個方法會對向量執行線性搜尋,找出項目。我們更新了資料結構,改為依指令 ID 編列索引,讓 FindReferenceInfoOf 成為 O(1) 而非 O(n)。此外,我們預先配置了向量,避免調整大小。由於必須新增額外欄位來計算向量中插入的項目數,因此記憶體略有增加,但這是值得的犧牲,因為尖峰記憶體並未增加。這項變更將 LoadStoreAnalysis 階段的速度提升了 34% 至 66%,進而使編譯時間縮短約 0.5% 至 1.8%。

我們在多個位置使用 HashSet 的自訂實作項目。建立這個資料結構需要相當長的時間,我們也找出原因了。多年前,只有少數幾個使用非常大的 HashSet 的地方會用到這種資料結構,而且經過調整,可針對這些地方進行最佳化。不過,現在的用途正好相反,只有少數項目,而且生命週期很短。這表示我們浪費了週期,因為我們建立了這個巨大的 HashSet,但只使用了幾個項目,隨後就捨棄了。這項變更可將編譯時間縮短約 1.3% 至 2%。此外,由於我們不再使用像以往一樣大的資料結構,記憶體用量也減少了約 0.5% 至 1%。

我們透過參照將資料結構傳遞至 Lambda,避免複製這些結構,進而將編譯時間縮短約 0.5% 至 1%。這項改善措施在原始審查中遭到忽略,並在程式碼集內存在多年。我們查看 pprof 中的設定檔後,發現這些方法會建立及毀損大量資料結構,因此進行調查並加以最佳化。

我們快取了計算值,加快編譯輸出內容的階段,因此編譯總時間縮短了約 1.3% 至 2.8%。遺憾的是,額外的記帳作業過於繁瑣,自動測試也提醒我們出現記憶體迴歸問題。後來,我們再次檢查同一段程式碼,並實作了新版本,不僅解決了記憶體回歸問題,還進一步改善了編譯時間,幅度約為 0.5% 至 1.8%。在第二次變更中,我們必須重構並重新構思這個階段的運作方式,才能擺脫其中一個資料結構。

最佳化編譯器有一個階段會內嵌函式呼叫,以提升效能。為選擇要內嵌的方法,我們會在執行任何運算前使用啟發式方法,並在完成工作後但最終內嵌前進行最終檢查。如果其中任何一項偵測到內嵌不值得 (例如會新增太多新指令),我們就不會內嵌方法呼叫。

我們將兩項檢查從「最終檢查」類別移至「啟發式」類別,以便在執行任何耗時的運算作業前,預估內嵌作業是否會成功。由於這是預估值,因此不一定準確,但我們已驗證過,新的啟發式方法涵蓋了先前內嵌的 99.9% 內容,且不會影響效能。其中一項新的啟發式方法與所需的 DEX 暫存器有關 (改善約 0.2% 至 1.3%),另一項則與指令數量有關 (改善約 2%)。

我們在多個位置使用 BitVector 的自訂實作項目。針對特定固定大小的位元向量,我們將可調整大小的 BitVector 類別替換為較簡單的 BitVectorView。這可消除部分間接性和執行階段範圍檢查,並加快位元向量物件的建構速度。

此外,BitVectorView 類別已根據基礎儲存類型進行範本化 (而非一律使用 uint32_t 做為舊版 BitVector)。這項做法可讓某些作業 (例如 Union()) 在 64 位元平台上一起處理的位元數加倍。編譯 Android 作業系統時,受影響函式的樣本總共減少了 1% 以上。這項作業是透過多項變更完成 [123456]

如果詳細說明所有最佳化做法,我們可能要花上一整天!如要瞭解更多最佳化做法,請參閱我們實施的其他變更:

結論

我們致力於提升 ART 的編譯速度,並已獲得顯著進展,讓 Android 更加流暢有效率,同時延長電池續航力並改善裝置散熱效果。我們透過認真找出並實作最佳化項目,證明在不影響記憶體用量或程式碼品質的情況下,大幅提升編譯時間是可行的。

我們的歷程包括使用 pprof 等工具進行剖析、願意疊代,有時甚至會放棄成效不彰的途徑。ART 團隊的共同努力不僅顯著縮短了編譯時間,也為未來的進展奠定了基礎。

所有這些改良功能都會在 2025 年底的 Android 更新中推出,並透過主線更新提供給 Android 12 以上版本。希望這次深入探討最佳化程序,能讓您進一步瞭解編譯器工程的複雜性與回報!

繼續閱讀