產品新訊

編譯速度提升 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)。舉例來說,在 64 位元平台上,這項功能可讓 Union() 等作業一次處理的位元數增加一倍。編譯 Android 作業系統時,受影響函式的樣本總共減少了 1% 以上。這項作業已在多項變更中完成 [123456]

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

結論

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

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

2025 年底的 Android 更新將提供上述所有改良功能,Android 12 以上版本則可透過主線更新取得。希望這次深入探索最佳化程序,能讓您進一步瞭解編譯器工程的複雜性與好處!

繼續閱讀