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 为什么需要代码压缩?——三个真实场景

光讲理论没意思,我举三个我碰过的真实案例:

  1. Flash 不够用:一个智能电表项目,功能迭代到 V3.0,Flash 只剩 200 字节。加一个新功能需要 500 字节。最后靠编译器优化等级从 -O0 调到 -Os,加上手动删了 3 个冗余函数,硬是挤出了 600 字节。项目保住了。
  2. 响应超时:一个电机控制项目,PWM 中断必须在 20μs 内完成。原始代码用了浮点运算,一次计算就 15μs。改成定点查表后,整个中断只用了 8μs。时间维度压下来了,电机再也不抖了。
  3. 电池续航:一个蓝牙信标,要求纽扣电池撑 1 年。原始代码每秒广播一次,CPU 一直跑在 active 模式。优化后改成每 5 秒广播一次,中间深度休眠。代码没怎么变,功耗从 200μA 降到 15μA。

这三个场景说明什么?代码压缩不是炫技,是解决实际问题的手段。

1.4 代码压缩的知识体系

我把整个课程的知识结构画了张图,你先有个整体印象:

代码压缩知识体系 代码压缩知识体系 代码压缩核心目标 空间维度 时间维度 功耗维度 死代码消除 常量折叠 函数内联 数据压缩 循环展开 指令调度 分支预测 内存对齐 减少指令数 少访存 休眠模式 避总线竞争 三个维度互相牵制,优化需要全局权衡 Flash 不够用 响应超时 电池续航不足

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 本章小结

咱们这一章就讲这些。核心就三句话:

  • 嵌入式系统资源极度受限,代码压缩是必修课
  • 压缩有三个维度:空间、时间、功耗,它们互相牵制
  • 理解编译器在做什么,比手写优化更重要