Android 執行階段 (ART) 團隊在不影響編譯程式碼或任何尖峰記憶體迴歸的情況下,將編譯時間縮短了 18%。這項改善措施是 2025 年計畫的一部分,旨在縮短編譯時間,同時不影響記憶體用量或編譯程式碼的品質。
最佳化編譯時間速度對 ART 來說至關重要。舉例來說,即時 (JIT) 編譯會直接影響應用程式效率和整體裝置效能。編譯速度越快,最佳化作業生效前的時間就越短,使用者體驗也更流暢、更靈敏。此外,無論是 JIT 還是 AOT,編譯時間速度提升,代表編譯過程中的資源消耗減少,進而延長電池續航力並降低裝置溫度,尤其是在低階裝置上。
部分編譯時間速度改善項目已於 2025 年 6 月的 Android 版本推出,其餘項目將於年底的 Android 版本推出。此外,所有 Android 12 以上版本的使用者,都能透過主線更新取得這些改善項目。
最佳化最佳化編譯器
最佳化編譯器時,一律需要權衡取捨。速度並非憑空而來,必須有所取捨。我們為自己設定了非常明確且具挑戰性的目標:加快編譯器速度,但不能造成記憶體迴歸,更重要的是,不能降低產生的程式碼品質。如果編譯器速度變快,但應用程式執行速度變慢,就表示我們失敗了。
我們願意投入的資源,就是自己的開發時間,深入探究、調查並找出符合這些嚴格標準的巧妙解決方案。接著,我們來進一步瞭解如何找出需要改善的部分,以及如何針對各種問題找出合適的解決方案。
找出值得採用的最佳化建議
您必須先評估指標,才能開始進行最佳化。否則,你永遠無法確定是否有所改善。幸運的是,只要採取一些預防措施,例如在變更前後使用同一部裝置進行測量,並確保裝置不會因過熱而降頻,編譯時間速度就會相當一致。此外,我們也提供確定性評估指標 (例如編譯器統計資料),協助您瞭解幕後運作情形。
為了做出這些改善,我們犧牲了開發時間,因此希望盡可能加快疊代速度。這表示我們挑選了幾款具代表性的應用程式 (包括第一方應用程式、第三方應用程式和 Android 作業系統本身),用來設計解決方案的原型。後來,我們透過廣泛的手動和自動測試,驗證最終實作成果確實值得。
接著,我們會在本機手動編譯這組精選 APK,取得編譯設定檔,並使用 pprof 以視覺化方式呈現時間花費。
pprof 中的設定檔火焰圖範例
pprof 工具非常強大,可讓我們對資料進行切片、篩選和排序,例如查看哪些編譯器階段或方法耗費最多時間。我們不會詳細說明 pprof 本身,只要知道長條越大,代表編譯時間越長即可。
其中一個檢視畫面是「由下而上」,可查看哪些方法耗費最多時間。在下圖中,我們可以看到名為 Kill 的方法,占編譯時間的 1% 以上。本文稍後也會討論其他幾種熱門方法。
由下往上查看設定檔
在最佳化編譯器中,有一個階段稱為全域值編號 (GVN)。您不必擔心它整體的運作方式,但相關部分是知道它有一個名為「Kill」的方法,會根據篩選器刪除一些節點。這項作業必須逐一檢查所有節點,因此相當耗時。我們注意到,在某些情況下,無論當時有哪些節點處於運作狀態,我們都能預先知道檢查結果會是 false。在這種情況下,我們可以完全略過疊代,將疊代率從 1.023% 降至約 0.3%,並將 GVN 的執行階段縮短約 15%。
實施值得採行的最佳化建議
我們介紹了如何評估及偵測時間花費,但這只是開始。下一步是瞭解如何最佳化編譯時間。
一般來說,在上述「Kill」的情況下,我們會查看如何疊代節點,並透過平行處理或改善演算法本身等方式,加快疊代速度。事實上,我們一開始就是這麼做的,但當我們找不到任何可執行的操作時,我們才意識到「等一下…」,並發現解決方案是 (在某些情況下) 根本不進行疊代!進行這類最佳化時,很容易見樹不見林。
在其他情況下,我們使用了幾種不同的技術,包括:
- 使用經驗法則判斷最佳化作業是否無法產生有價值的結果,進而略過
- 使用額外的資料結構快取運算資料
- 變更目前的資料結構,加快速度
- 延遲計算結果,避免在某些情況下出現循環
- 使用正確的抽象化,不必要的功能可能會降低程式碼速度
- 避免透過多次載入追蹤常用指標
如何判斷是否值得進行最佳化?
這就是厲害的地方,你不需要。偵測到某個區域耗用大量編譯時間,並投入開發時間嘗試改善後,有時您可能無法找到解決方案。也許是無計可施、實作時間太長、會大幅降低另一項指標、增加程式碼集複雜度等。這篇網誌文章中介紹的每項成功最佳化措施,背後都有無數個未成功的嘗試。
如果您也遇到類似情況,請盡可能減少工作量,並嘗試估算指標的改善程度。依序為:
- 根據已收集的指標或直覺進行估算
- 使用快速粗略的原型進行估算
- 導入解決方案。
別忘了評估解決方案的缺點。舉例來說,如果您要使用額外的資料結構,願意使用多少記憶體?
深入探索
廢話不多說,現在就來看看我們實施了哪些異動。
我們調整並最佳化了 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%。這項作業是透過多項變更完成 [1、2、3、4、5、6]
如果我們詳細討論所有最佳化功能,可能要花上一整天!如果您想進一步最佳化,可以參考我們實作的其他變更:
- 新增記錄,將編譯時間縮短約 0.6% 至 1.6%。
- 延遲運算資料,盡可能避免循環。
- 重構程式碼,在不使用預先運算作業時略過該作業。
- 如果可以輕鬆從其他地方取得配置器,請避免使用某些依附元件載入鏈結。
- 另一個新增檢查以避免不必要工作的例子。
- 在暫存器分配器中,避免頻繁分支暫存器類型 (核心/FP)。
- 確保在編譯時初始化部分陣列。請勿依賴 clang 執行這項操作。
- 清除部分循環。使用 clang 可更有效最佳化的範圍迴圈,因為 clang 不需要因迴圈副作用而重新載入容器的內部指標。避免在迴圈中透過每個輸入的內嵌 `InputAt(.)` 呼叫虛擬函式 `HInstruction::GetInputRecords()`。
- 利用編譯器最佳化,避免使用訪客模式的 Accept() 函式。
結論
我們致力提升 ART 的編譯時間速度,並取得顯著成效,不僅讓 Android 運作更流暢、更有效率,還能延長電池續航力及改善裝置散熱。我們努力找出並實施最佳化措施,證明瞭在不影響記憶體用量或程式碼品質的情況下,可以大幅縮短編譯時間。
我們使用 pprof 等工具進行剖析、願意反覆測試,有時甚至會放棄成效不彰的方法。ART 團隊的共同努力不僅大幅縮短編譯時間,也為日後的進展奠定基礎。
上述所有改善項目都包含在 2025 年底的 Android 更新中,並透過主線更新提供給 Android 12 以上版本。希望這篇深入探討最佳化程序的文章,能讓您深入瞭解編譯器工程的複雜性和成果!
-
產品最新消息隨著代理式開發加速發展,Android 工程技術也邁入令人期待的新境界:從輔助程式碼補全,到代理依據您的指示執行端對端作業。您可以自由選擇要使用的模型和代理,享有充分彈性。
Jason Buchel • 閱讀時間:3 分鐘 -
產品最新消息今天,我們分享了如何將「不只是商店」的願景推向新階段。Google Play 致力於滿足使用者需求,包括推出「個人中心」分頁,成為每月超過 2.5 億活躍使用者的個人中心,以及整合遊戲平台,連結超過 7 億個玩家個人資料。
Paul Feng • 閱讀時間:3 分鐘 -
產品最新消息我們很高興能透過 Googlebook 協助使用者表達自我,並取得這些工具。
Cyrus Stoller • 閱讀時間:2 分鐘
每週透過電子郵件接收最新的 Android 開發洞察資訊。