1. 代码压缩基础:为什么需要代码压缩?
各位同学,咱们今天聊聊代码压缩。你可能觉得——代码能跑就行,压缩它干嘛?
我刚开始做嵌入式那会儿,也这么想。直到有一次,一个简单的温控器项目,Flash 只剩 2KB 了,功能还差一截没写完。项目经理盯着我,客户催着要,那叫一个焦头烂额。从那以后,我再也不敢小看代码压缩了。
说白了,嵌入式系统不是 PC。它的资源是「挤」出来的,不是「堆」出来的。
1.1 嵌入式系统的资源约束
咱们先看看嵌入式系统到底有多「穷」。我列个表,你感受一下:
| 资源类型 | 典型嵌入式设备 | 对比 PC/手机 | 差距倍数 |
|---|---|---|---|
| Flash(代码存储) | 32KB ~ 512KB | 256GB ~ 1TB | 约 10⁶ 倍 |
| RAM(运行内存) | 4KB ~ 64KB | 8GB ~ 32GB | 约 10⁶ 倍 |
| CPU 主频 | 8MHz ~ 200MHz | 2GHz ~ 4GHz | 约 10~100 倍 |
| 功耗预算 | 1mW ~ 500mW | 5W ~ 65W | 约 10~1000 倍 |
看到没?Flash 和 RAM 差了百万倍。你 PC 上多写个循环无所谓,嵌入式里多一个字节可能就装不下了。
核心观点:嵌入式开发本质上是在「资源天花板」下做权衡。代码压缩不是锦上添花,而是生存技能。
1.2 代码压缩的三个维度
代码压缩到底压什么?我习惯从三个维度看:时间、空间、功耗。这三个维度互相牵制,你压了一个,另外两个可能跟着变。
空间维度——省 Flash,省 RAM
这是最直观的。代码体积小了,就能塞进更便宜的芯片。我见过一个项目,就因为代码从 64KB 压到 48KB,芯片型号降了一档,单颗成本省了 0.3 美元。批量 10 万片,你算算省了多少?
空间压缩的手段包括:
- 死代码消除——把没调用的函数删掉
- 常量折叠——编译时算好结果,省掉运行时计算
- 函数内联——小函数直接展开,省掉调用开销
- 数据压缩——查表代替计算,用空间换时间
我的经验:空间压缩最容易见效,但也最容易引入 bug。我曾经删了一个「看起来没用」的初始化函数,结果那个函数是中断向量表的一部分……板子直接变砖。删代码前,先确认调用链。
时间维度——跑得快,响应及时
嵌入式系统很多是实时系统。传感器数据来了,你得在 1ms 内处理完,否则控制就失效了。
时间优化不是单纯让 CPU 跑更快,而是让关键路径更短。比如:
- 循环展开——减少循环控制开销
- 指令调度——让流水线不空转
- 分支预测优化——减少跳转惩罚
- 内存对齐——避免非对齐访问的额外周期
你想想看,一个中断服务函数如果多花 10μs,系统可能就丢数据了。时间维度压的是「确定性」。
功耗维度——省电,就是省钱
这个维度很多人容易忽略。电池供电的设备,功耗是命门。
代码怎么影响功耗?
- 减少指令数——CPU 少干活,自然省电
- 减少内存访问——访存比计算耗电得多
- 利用休眠模式——没事就睡,有事再醒
- 避免总线竞争——减少等待重试的能耗
我记得有个穿戴设备项目,优化前待机 3 天,优化后撑了 10 天。怎么做到的?就是把轮询改成中断触发,CPU 大部分时间在睡觉。代码没少几行,功耗降了 70%。
注意:三个维度经常互相冲突。空间压得太狠,可能引入额外计算,反而更耗电、更慢。时间优化过头,代码体积膨胀,Flash 装不下。做压缩,本质是找平衡点。
1.3 为什么需要代码压缩?——三个真实场景
光讲理论没意思,我举三个我碰过的真实案例:
- Flash 不够用:一个智能电表项目,功能迭代到 V3.0,Flash 只剩 200 字节。加一个新功能需要 500 字节。最后靠编译器优化等级从 -O0 调到 -Os,加上手动删了 3 个冗余函数,硬是挤出了 600 字节。项目保住了。
- 响应超时:一个电机控制项目,PWM 中断必须在 20μs 内完成。原始代码用了浮点运算,一次计算就 15μs。改成定点查表后,整个中断只用了 8μs。时间维度压下来了,电机再也不抖了。
- 电池续航:一个蓝牙信标,要求纽扣电池撑 1 年。原始代码每秒广播一次,CPU 一直跑在 active 模式。优化后改成每 5 秒广播一次,中间深度休眠。代码没怎么变,功耗从 200μA 降到 15μA。
这三个场景说明什么?代码压缩不是炫技,是解决实际问题的手段。
1.4 代码压缩的知识体系
我把整个课程的知识结构画了张图,你先有个整体印象:
1.5 一个简单的代码压缩示例
光说不练假把式。咱们看一段实际代码,感受一下压缩前后的差别:
// 压缩前:直观但冗余
int calc_avg(int *data, int len) {
int sum = 0;
for (int i = 0; i < len; i++) {
sum = sum + data[i];
}
return sum / len;
}
// 压缩后:编译器 -Os 优化后的等效逻辑
// 1. 循环变量 i 被优化掉,直接用指针
// 2. 除法被替换为移位(如果 len 是 2 的幂)
// 3. 中间变量 sum 直接使用寄存器
int calc_avg_opt(int *data, int len) {
int sum = 0;
int *end = data + len;
while (data < end) {
sum += *data++;
}
return sum >> 3; // 假设 len=8,除法变移位
}
你看,压缩后的代码:
- 少了循环变量 i,省了一个寄存器
- 指针自增代替索引访问,指令更少
- 除法变移位,快了不止一个数量级
当然,实际工作中你不用手写成这样。编译器会帮你做大部分。但你要知道编译器在做什么,才能写出让它「好优化」的代码。
我的建议:先写可读性好的代码,再用编译器优化。不要一开始就写「聪明」的代码——那往往最难维护,也最难进一步优化。
1.6 本章小结
咱们这一章就讲这些。核心就三句话:
- 嵌入式系统资源极度受限,代码压缩是必修课
- 压缩有三个维度:空间、时间、功耗,它们互相牵制
- 理解编译器在做什么,比手写优化更重要