3、CPU功耗分析:CPU频率与电压关系、C-State与P-State、调度器对功耗的影响、CPU负载跟踪
好,我们进入第三章。CPU功耗,这可以说是整个Android功耗优化的核心战场。你想想看,手机里最耗电的芯片就是它。我这些年调过的功耗问题,十有八九最后都追到了CPU头上。
这一章,我会把CPU功耗的几个关键维度拆开来讲。频率电压关系、C-State和P-State、调度器的影响,还有负载跟踪。嗯,每个点都是实战中绕不开的坑。
3.1 CPU频率与电压的关系
先聊个基础但极其重要的事:频率和电压的关系。
CPU要跑在更高频率,就得有更高的电压。这不是芯片厂商拍脑袋定的,而是物理规律决定的。CMOS电路的动态功耗公式是:
P = C × V² × f
这里C是负载电容,V是电压,f是频率。注意那个V的平方——电压对功耗的影响是指数级的。所以,哪怕只是降一点点电压,省电效果都非常明显。
我在项目中遇到过这么一件事。某款新平台,CPU跑在2.0GHz时电压需要0.85V,但降到1.8GHz时电压只需要0.75V。算下来,频率只降了10%,功耗却降了将近30%。这就是为什么系统没事就喜欢把频率往下压。
每个芯片出厂时都会有一个频率-电压表(也就是DVFS表)。这张表是芯片厂商通过大量测试标定出来的。我建议你拿到新平台后,第一件事就是去翻这个表。看看每个频点对应的电压是不是合理。有些厂商为了跑分好看,会把电压给得偏高,这其实是在牺牲续航。
我的习惯:在调优初期,我会手动把高频段的电压往下微调0.01V-0.02V,然后跑稳定性测试。如果没问题,就说明原厂给的电压有余量。当然,这需要芯片体质支持,量产时得谨慎。
3.2 C-State与P-State
这两个概念,说白了就是CPU的「休息」和「工作」状态。
P-State(Performance State):CPU在工作时的不同性能档位。比如1.2GHz、1.5GHz、1.8GHz,每个频点就是一个P-State。系统根据负载情况,在P-State之间切换,这就是DVFS(动态电压频率调整)。
C-State(C-State):CPU在空闲时的不同睡眠深度。C0是正常工作,C1是浅度睡眠,C2更深,C3、C4...一直到最深度的C-State。深度越深,唤醒延迟越大,但省电效果也越好。
我画个简单的对照表:
| 状态类型 | 含义 | 功耗 | 唤醒延迟 |
|---|---|---|---|
| C0 | 正常工作 | 高 | 0 |
| C1 | HALT(暂停) | 中 | 几微秒 |
| C2 | 时钟关闭 | 低 | 几十微秒 |
| C3 | 电压降低 | 极低 | 几百微秒 |
这里有个常见的误区。很多人觉得,让CPU尽量进深睡眠就能省电。其实不一定。如果CPU频繁在C0和深睡眠之间切换,每次唤醒都要花时间、花能量,反而可能更费电。
我曾经踩过的坑:有个项目,后台有个定时器每10ms唤醒一次CPU。CPU每次都从C3深度睡眠被唤醒,结果功耗比一直待在C1还高。后来我把定时器周期改到100ms,并让CPU在空闲时只进C1,功耗反而降下来了。
所以,C-State的选择是个权衡。你要根据实际场景来定。比如播放音乐时,CPU需要频繁响应,那就别让它进太深的睡眠。而待机时,可以大胆让它睡到C3甚至更深。
3.3 调度器对功耗的影响
调度器,说白了就是决定「哪个任务跑在哪个CPU核心上」。它对功耗的影响,比你想象的要大得多。
现在的手机都是大小核架构(big.LITTLE)。大核性能强但功耗高,小核性能弱但功耗低。调度器如果不够聪明,把轻量级任务扔到大核上跑,那就是在浪费电。
我举个例子。一个后台同步任务,只需要100MHz的算力。如果调度器把它放在大核上,大核可能跑在500MHz,功耗是50mW。但如果放在小核上,小核跑300MHz就够了,功耗只有15mW。差距三倍多。
Android的调度器,从早期的CFS(完全公平调度器)到现在的EAS(能耗感知调度器),一直在进化。EAS的核心思想就是:在保证性能的前提下,优先选择能效比更高的CPU核心。
EAS是怎么做到的?它依赖一个关键数据:CPU的能耗模型。这个模型描述了每个CPU核心在不同频率下的功耗。调度器根据任务的负载,估算出它在不同核心上运行所需的功耗,然后选择最优解。
// 能耗模型示例(伪代码)
struct energy_model {
unsigned long capacity; // 算力
unsigned long power; // 功耗(mW)
};
// 大核在1.5GHz时的能耗
struct energy_model big_core_1500 = {
.capacity = 800,
.power = 500,
};
// 小核在1.0GHz时的能耗
struct energy_model little_core_1000 = {
.capacity = 400,
.power = 150,
};
你看,大核算力是小核的两倍,但功耗却是三倍多。所以,对于算力需求在400以下的任务,用小核跑明显更划算。
我建议:在调优时,可以用 sysfs 接口查看当前调度器的决策。比如 /sys/kernel/debug/sched_features 可以查看EAS是否开启。如果没开,赶紧打开,省电效果立竿见影。
3.4 CPU负载跟踪
负载跟踪,是调度器和DVFS做决策的基础。如果负载算不准,后面的一切都是瞎忙活。
Linux内核里,负载跟踪经历了几个阶段。早期用的是平均负载,就是统计一段时间内CPU的忙碌比例。但这种方式太粗糙了,没法区分「持续高负载」和「突发高负载」。
后来引入了PELT(Per-Entity Load Tracking)。PELT会跟踪每个调度实体的负载,并且用指数衰减的方式计算历史负载的影响。简单说,就是最近的负载权重更大,过去的负载权重越来越小。
PELT的计算公式大致是这样的:
load = load_prev * (1 - y) + load_current * y
其中y是衰减系数,通常取0.5。这样,每过一个周期,历史负载的影响就减半。嗯,说白了就是「最近1秒的负载比10秒前的负载重要得多」。
我在项目中遇到过一个问题。某个应用在启动时会有一个短暂的峰值负载,然后迅速降下来。如果用平均负载,系统会认为负载很高,把频率抬上去。但用PELT,系统能快速感知到负载下降,及时把频率降下来。这就是PELT的优势。
注意:PELT虽然好,但也不是万能的。如果负载变化太快(比如每1ms就波动一次),PELT的衰减速度可能跟不上。这时候就需要配合其他机制,比如窗口化负载跟踪,来更精确地捕捉瞬时负载。
负载跟踪的最终目的,是给调度器和DVFS提供准确的输入。调度器根据负载决定任务放在哪个核心,DVFS根据负载决定核心跑在哪个频率。两者配合好了,才能做到「该快的时候快,该省的时候省」。
好,这一章的内容就到这里。CPU功耗分析是个系统工程,频率电压、C-State/P-State、调度器、负载跟踪,每个环节都环环相扣。下一章,我们会深入GPU功耗,看看图形渲染这块怎么优化。