Android CPU 使用率不准?一文搞懂 DVFS 降频对性能数据的影响

压测跑了 2 小时,CPU 一直稳定在 45%,看起来很健康。 但用户反馈"越用越卡"。查了才发现:设备早就热降频到 408MHz 了,45% 的算力只有正常模式的四分之一。 CPU% 没变,性能已经崩了。


一个真实场景

做长时间稳定性压测时,我们经常看到这样的 CPU 曲线:

matlab 复制代码
时间     CPU%    体感
0min     40%     流畅
30min    42%     流畅
60min    45%     开始卡顿
90min    44%     明显卡顿
120min   43%     严重卡顿

CPU% 几乎没变,但卡顿越来越严重。如果只看 CPU 使用率,根本发现不了问题。

查 CPU 频率才发现真相:

matlab 复制代码
时间     CPU%    频率        等效算力
0min     40%     1800MHz     40.0%
30min    42%     1400MHz     32.7%
60min    45%     800MHz      20.0%
90min    44%     408MHz      10.0%
120min   43%     408MHz      9.7%

60 分钟后设备因为发热触发了热降频,CPU 频率从 1800MHz 一路降到 408MHz。虽然 CPU% 还是 45%(占了 45% 的时间片),但每个时间片能做的运算量只有原来的四分之一

这就是 CPU 采集中最容易误导人的坑:CPU% 只衡量"忙不忙",不衡量"干了多少活"。


为什么会降频:DVFS 机制

Android 设备通过 DVFS(Dynamic Voltage and Frequency Scaling,动态电压频率调节) 来平衡性能和功耗。内核中的 CPU 调频器(governor)根据负载动态调整频率。

常见的调频器:

调频器 策略 典型场景
performance 锁定最高频率 跑分/性能测试
powersave 锁定最低频率 息屏待机
schedutil 根据调度器负载动态调 Android 默认(8.0+)
interactive 根据 CPU 忙碌度快速升频 旧版 Android 默认

除了 governor 调频,还有热降频(thermal throttling)------设备温度超过阈值后,内核强制把频率压低。这是长时间压测中最常见的降频原因。

查看当前频率:

bash 复制代码
# 各核当前频率(单位 KHz)
adb shell cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
# → 1800000(1.8GHz)或 408000(408MHz)

# 各核最高频率
adb shell cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq

# 各核最低频率
adb shell cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq

# 可用频率档位
adb shell cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies
# → 408000 600000 816000 1008000 1200000 1416000 1608000 1800000

查看热降频状态:

bash 复制代码
# 当前温度(不同设备路径不同)
adb shell cat /sys/class/thermal/thermal_zone0/temp
# → 65000(即 65°C)

# 当前热限制策略
adb shell cat /sys/class/thermal/thermal_zone0/policy

CPU% 的本质:时间片占比,不是算力占比

理解降频问题的关键,是搞清楚 CPU% 到底在度量什么。

/proc/stat 记录的是**时间片(tick)**的分配。CONFIG_HZ=100 时,每秒产生 100 个 tick,每个 tick 10ms。CPU% = 45% 意味着"在这些 tick 中,有 45% 的时间 CPU 在执行任务"。

但这些 tick 不关心频率。无论 CPU 跑在 408MHz 还是 1800MHz,1 个 tick 就是 1 个 tick。区别在于:

  • 1800MHz 时,1 个 tick(10ms)能执行约 1800 万条指令
  • 408MHz 时,1 个 tick(10ms)只能执行约 408 万条指令

同样是"用了 45 个 tick",前者完成的计算量是后者的 4.4 倍

用一个类比:CPU% 就像"一天中有多少小时在工作",频率就像"每小时的工作效率"。一个人每天工作 8 小时(CPU 占用率不变),但如果他从精力充沛(高频)变成了困倦迷糊(低频),实际产出可能差好几倍。


Normalized CPU%:让使用率反映真实算力

解决思路很直接------把频率因素加进去。行业里这个指标叫 Normalized CPU%(归一化 CPU 使用率),PerfDog 等工具也是这么做的。

单核场景的公式

scss 复制代码
Normalized CPU% = Raw CPU% × (当前频率 / 最高频率)
ini 复制代码
场景 A:Raw CPU 50% @ 1800MHz
Normalized = 50% × (1800/1800) = 50.0%

场景 B:Raw CPU 50% @ 408MHz
Normalized = 50% × (408/1800) = 11.3%

场景 B 虽然占了 50% 的时间片,但实际算力只有满血状态的 11.3%。

多核场景:实际设备中各核频率经常不同,需要用"频率比"来统一衡量:

scss 复制代码
频率比 = Σ(各在线核当前频率) / Σ(各核最高频率)

Normalized CPU% = Raw CPU% × 频率比

比如 4 核设备,cpu0~cpu3 最高均为 1800MHz,当前分别跑在 1200/408/1800/1800MHz:

ini 复制代码
频率比 = (1200 + 408 + 1800 + 1800) / (1800 × 4) = 0.723

Raw CPU 60% → Normalized = 60% × 0.723 = 43.4%

虽然 CPU 忙碌度有 60%,但因为部分核心降频了,实际算力打了七折。

关于分母"各核最高频率"------和第 6 篇不矛盾

第 6 篇说 Raw CPU% "无需知道核数"------因为 /proc/stat 汇总行只含在线核 tick,Δuse/Δtotal 在时间维度上天然正确。

这里 Normalized CPU% 的分母用了全部核心 的最高频率(含关掉的核),是故意的------它度量的是"设备满血状态下,你用了多少算力"。如果 4 核关了 2 核,Raw CPU% = 100%(可用资源耗尽),而 Normalized CPU% = 50%(设备只发挥了一半潜力)。两个指标度量不同维度,一个看"忙不忙",一个看"产出了多少"。

频率比的完整数学推导(包含关核场景),见下篇第 8 篇:CPU 单核分析 。与 PerfDog / Linux PELT 的公式对齐,见第 9 篇:CPU 采集落地方案


大小核的隐患:频率相同,算力不同

上面的 Normalized CPU% 只考虑了频率维度。但现代 Android 几乎都是 big.LITTLE 架构

makefile 复制代码
小核 (LITTLE): cpu0~cpu3, 最高 1800MHz, Cortex-A55
大核 (big):    cpu4~cpu5, 最高 2400MHz, Cortex-A76
超大核:        cpu6~cpu7, 最高 3000MHz, Cortex-X2

关键问题:同样跑在最高频率,大核的 IPC(每时钟周期指令数)是小核的 2~3 倍

也就是说:

  • 进程跑在小核 cpu0 上,CPU% = 80%,实际性能可能只相当于大核 cpu6 的 20~25%
  • 同一个解码任务,调度到小核可能卡顿,调度到大核完全流畅

纯频率加权解决了"降频导致数据失真"的问题,但无法区分大小核的微架构差异 。要精确衡量,需要引入核心性能系数(IPC 系数) ------这部分涉及 per-cpu 时间分摊、性能系数标定、CV 负载均衡度等,在下篇 第 8 篇:CPU 单核分析 中详细展开。

对于大部分测试场景,频率加权已经覆盖了最主要的误差来源(降频导致的数据失真)。大小核 IPC 差异属于进一步精细化的范畴。


频率驻留时间:time_in_state

除了实时频率,还有一个重要数据源------频率驻留时间统计

bash 复制代码
adb shell cat /sys/devices/system/cpu/cpu0/cpufreq/stats/time_in_state
# 频率(KHz)   驻留时间(10ms为单位)
# 408000      52341
# 600000      1203
# 816000      892
# 1008000     2451
# 1200000     5623
# 1416000     3892
# 1608000     2341
# 1800000     8923

这告诉你从开机到现在,cpu0 在每个频率档位分别待了多长时间。做两次差值就能得到"这段测试时间内,CPU 在各频率档位的分布"。

典型应用场景

场景 1:热降频严重程度量化

yaml 复制代码
正常运行(前 30 分钟):
  1800MHz: 70%   1200MHz: 20%   408MHz: 10%

热降频后(60~90 分钟):
  1800MHz: 5%    1200MHz: 15%   408MHz: 80%

一目了然:80% 的时间被压在最低频率,性能衰减非常严重。

场景 2:不同版本/配置的功耗对比

功耗大致正比于 频率² × 电压 (DVFS 调频的同时也在调电压)。频率差 4 倍时,功耗差可达 10~16 倍

通过 time_in_state 可以算出"加权平均频率",比直接看瞬时频率更能反映整体能效:

python 复制代码
def weighted_avg_freq(time_in_state_delta):
    """计算加权平均频率"""
    total_time = sum(time_in_state_delta.values())
    if total_time == 0:
        return 0
    weighted = sum(freq * duration for freq, duration in time_in_state_delta.items())
    return weighted / total_time

# 结果:
# 正常运行:avg_freq ≈ 1500MHz
# 热降频后:avg_freq ≈ 550MHz

真实案例:CPU 45% 却严重卡顿的排查

回到开头的场景,完整的排查思路:

第一步:发现问题

压测 2 小时后用户反馈"越来越卡",但 CPU 监控曲线一直平稳在 45% 左右。

第二步:叠加频率数据

同时采集 CPU% 和各核频率,发现:

ini 复制代码
0~30min:   Raw 40% × freq_ratio 0.95 = Normalized 38%    → 正常
30~60min:  Raw 42% × freq_ratio 0.72 = Normalized 30%    → 开始衰减
60~90min:  Raw 45% × freq_ratio 0.35 = Normalized 16%    → 严重衰减
90~120min: Raw 44% × freq_ratio 0.23 = Normalized 10%    → 几乎不可用

Normalized CPU 从 38% 一路降到 10%------算力跌了 74%。

第三步:确认降频原因

bash 复制代码
adb shell cat /sys/class/thermal/thermal_zone0/temp
# → 78000(78°C,超过降频阈值 70°C)

设备在高负载下持续发热,温度超过 70°C 后触发热降频,CPU 频率被强制压到最低档。

第四步:定位根因

  • 不是 CPU 算法问题,是散热/热设计问题
  • 解决方案:优化散热(加散热片/风扇)、降低持续负载、或在热降频前主动降低画质/帧率

如果只看 Raw CPU%,这个问题永远发现不了。 CPU% 会告诉你"一切正常",但用户已经卡到无法使用。


落地建议:三条线同时看

把 CPU 使用率和频率放在一起采集,每轮同时输出三个指标:

arduino 复制代码
Raw CPU%        → 衡量"CPU 有多忙"(时间片维度)
频率比          → 衡量"CPU 跑得多快"(频率维度)
Normalized CPU% → 衡量"CPU 做了多少活"(算力维度)

三个指标同时上报到图表,叠加展示:

  • Raw CPU% 曲线发现"是否忙"
  • 频率比曲线发现"是否降频"
  • Normalized CPU% 曲线发现"实际算力变化"

三条线同时看,任何异常都藏不住。

单核粒度的归一化方案见第 8 篇 ,完整的采集代码和平台落地方案见第 9 篇


小结

维度 Raw CPU% Normalized CPU%
度量含义 时间片占比 等效算力占比
降频场景 看不出问题 直接反映算力衰减
大小核 IPC 差异 不区分 本篇不处理(见第 8、9 篇)
功耗关联 强(功耗 ∝ f² × V)
实现成本 每轮多读几个 sysfs 文件

一句话总结Raw CPU% 只告诉你"忙不忙",加上频率做 Normalized 才能告诉你"干了多少活"。

长时间压测、功耗分析、热性能评估------只要涉及频率变化的场景,Normalized CPU% 都比 Raw 值有意义得多。


下篇预告

CPU 单核分析:均值是最大的谎言

频率加权解决了降频失真,但整机均值掩盖了单核瓶颈------8 核平均 45%,其中一个小核已经 95% 满载。 下篇从数学推导出发,搞清楚整机 CPU% 和各核的真实关系,引入 CV 负载均衡度和 IPC 性能系数,补上空间维度的分析。


系列目录

  • 第 1 篇:内存泄漏自动检测(上)------采集层设计
  • 第 2 篇:内存泄漏自动检测(中)------检测层设计
  • 第 3 篇:内存泄漏自动检测(下)------响应层设计
  • 第 4 篇:Android 内存采集避坑指南
  • 第 5 篇:Android CPU 使用率采集入门
  • 第 6 篇:CPU 采集的 8 个坑
  • 第 7 篇(本篇):CPU 降频了,你的采集数据还准吗?
  • 第 8 篇(下一篇):CPU 单核分析:均值是最大的谎言
  • 第 9 篇:CPU 采集落地:从公式到平台的完整方案

我是测试工坊,专注 Android 系统级性能工程。 如果你也遇到过"CPU 不高但就是卡"的场景,欢迎评论区交流 👇 关注我,后续更新不迷路。

相关推荐
城东米粉儿2 小时前
Android Hilt 笔记
android
醉饮千觞不知愁2 小时前
Android Lifecycle的事件与状态映射关系
android·kotlin
千里马学框架3 小时前
app性能优化:优化布局层次结构
android·面试·性能优化·framework·分屏·布局·小米汽车
dustcell.3 小时前
高性能web服务器
android·服务器·前端
zh_xuan3 小时前
React Native Demo
android·javascript·react native·ts
zh_xuan3 小时前
kotlin 挂起函数2
android·kotlin·挂起函数
kyle~3 小时前
MySQL基础知识点与常用SQL语句整理
android·sql·mysql
XiaoLeisj4 小时前
Android RecyclerView 实战:从基础列表到多类型 Item、分割线与状态复用问题
android·java
zh_xuan4 小时前
kotlin async异步协程构建器
android·kotlin·协程