一段程式碼以前是好的,但經過多年迭代之後,雖然程式本身沒改,卻可能反而變成問題,這就是這次要聊的主題:
一個原本為了改善 Android UI 流暢度而引入的排程器最佳化,幾年後反而讓 SoC 功耗最高增加 20% 。

事情的起因是 Linux 排程器裡的 cpu_util() boost 機制。最近傳音控股的工程師 Hongyan Xia 在 LKML 提交了一個 patch:
bash 体验AI代码助手 代码解读复制代码[PATCH] sched/fair: Revert boost in cpu_util()
這個 patch 主要是把
cpu_util()裡基於runnable_avg的 boost 邏輯移除。雖然看起來像是一個排程器的小改動,但背後其實牽涉到 PELT、schedutil、Android 圖形管線、JankBench、ADPF、廠商功耗策略,以及 Linux 主線核心和 Android 真實裝置之間的驗證偏差等問題。
大致情況是,傳音工程師在升級 Linux 核心版本後,發現多款 Android 手機的 SoC 功耗明顯上升,而且在多種真實工作負載下都出現類似情況,不限於某個 App 或某顆晶片。他們測試的場景包括:
而出現問題的情況是:
於是他們開始做 git bisect,最後定位到 Linux CPU 排程器和 schedutil CPU 頻率調整路徑裡的 boost 邏輯。這個 boost 邏輯的關鍵點是:
過去 schedutil 主要看
util_avg,後來為了更積極回應 CPU contention,又額外看runnable_avg;如果runnable_avg暗示「排隊工作很多」,就提高 CPU 頻率。
也就是說,系統看到很多任務處於 runnable 狀態,就認為 CPU 可能不夠用,因此更積極地拉高頻率;但傳音工程師實測後發現,這種處理邏輯在現在的 Android 上很不合理,它讓 CPU 更積極提頻,卻沒有換來對應的效能收益。
這個問題得從 Linux 排程器聊起。Linux 裡有一個很核心的負載追蹤機制,叫 PELT,全名是 Per-Entity Load Tracking。它大致會追蹤兩個東西:
util_avg:任務真正拿到 CPU 執行的時間runnable_avg:任務處於 runnable 狀態的時間,包括正在執行,也包括排隊等待 CPU這兩個指標在 CPU 競爭場景下差異很大。舉例來說,如果一顆 CPU 上同時有 4 個任務在搶:
arduino 体验AI代码助手 代码解读复制代码Task A
Task B
Task C
Task D
如果它們都很想跑,但每個任務只能拿到 25% 的 CPU 時間,那麼每個任務的 util_avg 看起來都不高,從單一任務角度看:
arduino 体验AI代码助手 代码解读复制代码Task A 實際只執行 25%
Task B 實際只執行 25%
Task C 實際只執行 25%
Task D 實際只執行 25%
於是
util_avg會顯得偏低,但真實情況是 CPU 已經被塞滿了,這就是 boost 機制當初想解決的問題。
如果排程器只看「任務實際跑了多久」,就可能低估 CPU 需求;如果把「任務排隊等了多久」也算進來,就能更早發現 CPU contention,所以這個設計的初衷是:
util_avg反應慢,那就引入runnable_avg。
如果runnable_avg明顯高於util_avg,表示有任務在排隊。
任務在排隊,可能表示 CPU 頻率不夠。
那就讓 schedutil 更積極提頻。
但在 Android 上就有點不一樣。Android 手機常見的 CPU 調頻路徑一般可以簡化成:
這裡主要是 schedutil:每次排程器負載追蹤更新時,例如任務喚醒、任務遷移、時間推進,都會呼叫 schedutil 去更新硬體 DVFS 狀態。
也就是說,schedutil 本質上是一個「由排程器驅動的 CPU 調頻 governor」。它根據 CPU runqueue 上的 utilization 估算目前需要多少頻率,利用率越高,目標頻率越高。所以 cpu_util() 裡多加一個 boost,在 Android 上就不是一個無關緊要的小數值,它會直接影響 CPU 頻率選擇。
例如在目前主線程式裡,sugov_get_util() 就會呼叫:
ini 体验AI代码助手 代码解读复制代码util += cpu_util_cfs_boost(sg_cpu->cpu);
util = effective_cpu_util(...);
util = max(util, boost);
sg_cpu->util = sugov_effective_cpu_perf(...);
這代表 CFS 的 boost util 會進入 schedutil 的頻率決策鏈路;一旦 cpu_util_cfs_boost() 給出的值偏高,CPU 就更容易被推到高頻。
不過這套機制在過去的 Android 其實是有用的,因為當時 Android UI 的體驗常常「慢半拍」。早期 Android 的排程問題裡:
當年很多 Android 效能最佳化的思路都偏向「寧可早點提頻,也不要掉幀」。JankBench 這類工具也正是在這種背景下被用來驗證 UI 流暢度,它關注的是 Android 圖形管線,也就是使用者滑動、列表渲染、動畫等場景下的 jank。
所以在當年看來,boost 邏輯有它的歷史合理性,因為 PELT 慢、UI 負載短而急、schedutil 提頻慢,導致使用者看到卡頓,因此用 runnable_avg 提前補一腳油門。
但問題在於,時代變了,這也是這次傳音發現的結論:
也就是說,現在 runnable_avg boost 的場景在新一代 Android 手機上不成立,因為它的假設是:
runnable_avg 高
↓
CPU contention 高
↓
CPU 頻率不夠
↓
提高 CPU 頻率能改善體驗
但實測並不是這樣。runnable 多,不一定代表 CPU 頻率不夠。任務排隊可能是 CPU 忙,也可能是鎖競爭、執行緒喚醒風暴、Binder 排程、GPU 等待、記憶體頻寬壓力、thermal 限制,甚至是應用程式本身的執行緒模型不合理。
這時候排程器看到的是很多 runnable task,但系統真正的問題可能是:
所以如果瓶頸不在 CPU 頻率上,提高 CPU 頻率其實不會明顯提升效能。
另外,現在 Android 已經有更直接的 performance hint。過去系統需要靠排程器猜,但 Android 後來已逐步引入更明確的機制,例如 ADPF(Android Dynamic Performance Framework),它支援遊戲和效能敏感 App 更直接地與 Android 的功耗、溫控和 CPU 管理系統互動,而不是讓排程器只靠 runnable / util 盲猜。
所以從這個角度看,
runnable_avgboost 是一個比較粗糙的舊時代啟發式規則。
最後,現在廠商通常也有自己的一堆排程和提頻策略。畢竟 Android 手機不是裸 Linux,SoC 廠商、系統廠商、遊戲模式、Power HAL、thermal governor、GPU governor 都可能參與效能決策。
所以如果廠商已經對前台 App、遊戲執行緒、SurfaceFlinger、RenderThread 做了 hint 或 boost,Linux 排程器再根據 runnable_avg 加一層 boost,就很容易出現多層策略疊加,導致「過度提頻」。
傳音這次看到的現象就很像這種過度提頻:CPU 更常待在高頻,SoC 功耗明顯增加,但使用者可見效能沒有明顯變化。
所以這其實不算是 Bug,而是當年的最佳化到了今天反而成了負擔。現在系統更成熟了,方案也更多了,所以 boost 自然就成了時代的問題;而 Linux 主線希望機制通用,Android 裝置則希望整機體驗和續航最優,這些年 Android 發展太快,所以 Linux 層面沒跟上也算正常。
所以程式碼不是一開始能跑,就代表一直都能跑;過去的最佳化,在現在也可能變成負債。