第四讲:代码级功耗分析——循环优化、分支预测与函数调用开销
各位同学,咱们今天聊点实在的。
代码怎么写,芯片就怎么耗电。这不是玄学,是实实在在的物理规律。我做了十几年嵌入式,见过太多“功能跑通了,功耗炸了”的案例。今天咱们就掰开揉碎,讲讲循环、分支和函数调用这三个“电老虎”。
一、循环优化:别让CPU空转
循环是嵌入式代码里最常见的结构。但你知道吗?一个糟糕的循环,能让功耗翻倍。
我个人习惯,写循环前先问自己三个问题:
- 循环次数能不能提前确定?
- 循环体里有没有不必要的操作?
- 能不能用查表代替计算?
1.1 循环展开:用空间换时间
先看个例子。假设我们要对数组做累加:
// 原始循环
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += data[i];
}
// 循环展开(4次)
int sum = 0;
for (int i = 0; i < 100; i += 4) {
sum += data[i];
sum += data[i+1];
sum += data[i+2];
sum += data[i+3];
}
为什么展开后省电?因为循环控制(i++、比较、跳转)减少了75%。每次跳转,CPU都要刷新流水线,这玩意儿特别耗电。我在项目中遇到过,一个音频解码算法,展开后功耗降了12%。
1.2 减少循环内的函数调用
这个坑我踩过。曾经有个项目,循环里调了个判断函数:
// 糟糕的做法
for (int i = 0; i < 1000; i++) {
if (is_valid(data[i])) {
process(data[i]);
}
}
// 优化后
int (*valid_func)(int) = is_valid;
for (int i = 0; i < 1000; i++) {
if (valid_func(data[i])) {
process(data[i]);
}
}
你想想看,每次循环都去查函数地址、压栈出栈,CPU累不累?说白了,循环体越“瘦”,功耗越低。能内联的就内联,能用宏的就用宏。
二、分支预测:猜对了省电,猜错了费电
分支预测是现代CPU的“读心术”。猜对了,流水线跑得飞起;猜错了,整个流水线要清空重来。一次预测失败,浪费大约10-20个时钟周期。
为什么会这样?因为CPU为了提速,会提前把分支后面的指令预取进来。如果猜错了,这些预取工作全白干了,还要重新取指令、译码、执行。这就像你开车走错了路,得掉头重来——油耗自然高。
2.1 让分支可预测
CPU的分支预测器有个特点:它喜欢“规律”。比如:
// 难以预测:随机分布
if (data[i] > 0) { ... }
// 容易预测:大部分为真
if (data[i] > 0) { ... } // 假设90%的数据都大于0
我建议,写条件判断时,把大概率发生的情况放在前面。比如:
// 好的写法
if (likely(data[i] > 0)) { // 用likely/unlikely宏提示编译器
// 常见路径
} else {
// 异常路径
}
2.2 用查表代替分支
这是我最喜欢的手法。比如一个简单的符号函数:
// 分支版本
int sign(int x) {
if (x > 0) return 1;
else if (x == 0) return 0;
else return -1;
}
// 查表版本
int sign_table[3] = {-1, 0, 1};
int sign(int x) {
return sign_table[(x > 0) - (x < 0) + 1];
}
查表版本没有分支,CPU不用猜,功耗自然低。我在做电机控制算法时,用查表替换了十几个条件判断,整体功耗降了8%。
三、函数调用开销:别小看那点“零头”
很多新手觉得,函数调用不就是几条指令吗?能费多少电?
嗯,这里要注意。一次函数调用,背后是:压栈、保存寄存器、跳转、执行、恢复寄存器、出栈。如果函数很小,调用开销可能比函数体本身还大。
3.1 内联小函数
// 不内联
int add(int a, int b) {
return a + b;
}
// 内联
static inline int add(int a, int b) {
return a + b;
}
对于这种只有一两行的函数,内联后没有调用开销,代码直接展开。我建议,函数体小于5行的,都考虑内联。
3.2 减少参数传递
参数越多,压栈越重。ARM架构下,前4个参数用寄存器传,后面的就要压栈了。
// 糟糕:6个参数
void set_pwm(int ch, int freq, int duty, int deadtime, int phase, int polarity);
// 优化:用结构体
typedef struct {
int ch;
int freq;
int duty;
int deadtime;
int phase;
int polarity;
} pwm_config_t;
void set_pwm(pwm_config_t *cfg);
用指针传结构体,只压一个参数。我在项目中遇到过,一个频繁调用的配置函数,参数从8个减到1个指针后,功耗降了5%。
3.3 函数调用频率控制
有些函数没必要每次循环都调。比如:
// 每次循环都调
for (int i = 0; i < 100; i++) {
update_display(); // 每秒刷新60次就够了
process_data(data[i]);
}
// 优化:降低调用频率
for (int i = 0; i < 100; i++) {
if (i % 10 == 0) {
update_display(); // 每10次刷新一次
}
process_data(data[i]);
}
你想想看,显示刷新这种操作,人眼根本看不出区别。但CPU省下的电,电池能感受到。
四、实战总结:一个优化案例
最后,我分享一个真实案例。某款智能手表的心率算法,原始代码长这样:
for (int i = 0; i < SAMPLE_COUNT; i++) {
int val = read_sensor(i);
if (val > THRESHOLD) {
val = filter(val);
val = downsample(val);
buffer[i] = val;
} else {
buffer[i] = 0;
}
}
优化后:
// 循环展开4次
// 分支改为查表
// filter和downsample内联
// 减少read_sensor调用次数
for (int i = 0; i < SAMPLE_COUNT; i += 4) {
int v0 = read_sensor(i);
int v1 = read_sensor(i+1);
int v2 = read_sensor(i+2);
int v3 = read_sensor(i+3);
// 查表代替分支
buffer[i] = filter_table[v0 > THRESHOLD] ? filter(v0) : 0;
buffer[i+1] = filter_table[v1 > THRESHOLD] ? filter(v1) : 0;
buffer[i+2] = filter_table[v2 > THRESHOLD] ? filter(v2) : 0;
buffer[i+3] = filter_table[v3 > THRESHOLD] ? filter(v3) : 0;
}
结果:功耗降低了22%,算法运行时间缩短了35%。
好了,这一讲就到这里。下一讲咱们聊聊“内存访问对功耗的影响”——你会发现,读写内存比算算术费电多了。