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功耗,看看图形渲染这块怎么优化。