1、优化基础:为什么嵌入式代码需要优化?性能与体积的权衡。
做嵌入式开发的朋友,十有八九都遇到过这种场景:产品功能调通了,一测发现 Flash 剩 2KB,RAM 只剩 200 字节,CPU 跑在 80% 占用率。客户还催着加新功能。这时候你怎么办?
我早年做智能电表项目时就栽过这个坑。代码写完了,功能全对,结果一编译,Flash 超了 12%。项目经理盯着我看,那眼神我到现在都记得。从那以后,我养成了一个习惯——写代码之前先想清楚:这行代码到底要花多少资源?
说白了,嵌入式优化不是炫技,是生存技能。
1.1 嵌入式系统的资源约束
先看一组真实数据。我随便列几个常见 MCU 的资源规格:
| MCU 型号 | Flash | RAM | 主频 | 典型应用 |
|---|---|---|---|---|
| STM32F103C8T6 | 64 KB | 20 KB | 72 MHz | 工业控制 |
| ESP32 | 4 MB | 520 KB | 240 MHz | 物联网 |
| ATmega328P | 32 KB | 2 KB | 16 MHz | Arduino 入门 |
| GD32F303 | 128 KB | 32 KB | 120 MHz | 电机驱动 |
你看,最常用的 STM32F103,Flash 才 64KB。一首 MP3 都装不下。RAM 更可怜,20KB 连一张 320×240 的图片都存不了。这就是我们每天面对的现实。
你想想看,在这种环境下写代码,每一行都要精打细算。PC 程序员写个循环用 std::vector 随便 push_back,嵌入式里一个动态内存分配就可能把堆撑爆。
1.2 性能与体积:鱼和熊掌
这里有个核心矛盾:代码跑得快,往往体积就大;代码体积小,往往跑得慢。
为什么会这样?我举个例子。
假设你要计算一个数的平方。最直接的方式:
// 方式 A:直接计算
int square(int x) {
return x * x;
}
编译器可能会把它优化成一条乘法指令。体积小,速度快。
但如果你用查表法:
// 方式 B:查表法
const int lookup[256] = {0, 1, 4, 9, 16, ...};
int square(int x) {
return lookup[x];
}
查表法快不快?快,一次内存访问就出结果。但代价呢?256 个 int 占 1KB 的 Flash。如果输入范围是 0~65535,那就要 256KB——整个芯片的 Flash 都不够用。
这就是典型的空间换时间。
核心权衡原则:
- 时间优先:用查表、循环展开、内联函数——体积变大
- 空间优先:用函数复用、共享代码段、紧凑数据结构——速度变慢
- 平衡点:在满足实时性要求的前提下,尽量压缩体积
我在做车载 OBD 诊断协议栈时,就遇到过这种选择。协议栈要求响应时间小于 10ms,但 Flash 只剩 8KB。最后我选择了混合策略:高频调用的函数用查表加速,低频分支用紧凑的 if-else 链。效果不错,既满足了时序,又没超体积。
1.3 优化的三个层次
很多人一上来就盯着汇编看,其实没必要。优化的层次是有顺序的:
- 算法与数据结构——收益最大,往往能带来数量级的提升
- 编译器优化——零成本,改个编译选项就行
- 手写汇编/微架构优化——收益递减,只在关键路径使用
我见过最离谱的案例:有人花了两周手写汇编优化一个排序函数,结果把算法从冒泡改成快速排序,性能直接提升了 50 倍。两周 vs 两小时,你说哪个划算?
我的建议:先做算法级优化,再开编译器优化,最后才考虑手写汇编。这个顺序能帮你省下 80% 的优化时间。
1.4 什么时候必须优化?
不是所有代码都需要优化。我总结了几条判断标准:
| 场景 | 必须优化 | 可以暂缓 |
|---|---|---|
| Flash 占用 > 85% | ✅ 立即优化 | — |
| CPU 占用 > 70% | ✅ 需要优化 | — |
| 中断响应超时 | ✅ 必须优化 | — |
| 功能原型验证 | — | ✅ 先跑通再说 |
| 一次性量产固件 | — | ✅ 可适度优化 |
嗯,这里要注意:不要过早优化。我早期做项目时,总喜欢在写代码时就想着怎么优化,结果代码结构搞得一团糟,后期维护成本翻倍。后来我学乖了:先跑通,再测性能,最后针对瓶颈优化。
1.5 本章知识体系
下面这张图,是我对本章核心逻辑的总结。你可以把它当作后续学习的导航图:
1.6 一个真实的权衡案例
最后分享一个我亲身经历的项目。当时做一款便携式心电图仪,主控是 STM32F103,Flash 64KB,RAM 20KB。
算法团队给了一个数字滤波函数,用了 32 阶 FIR 滤波器。代码很漂亮,但问题来了:
- 滤波器系数表占 128 字节(32 个 float)
- 延迟线缓冲区占 128 字节
- 每次滤波需要 32 次乘加运算
RAM 还剩 3KB,Flash 还剩 8KB。看起来够用?
但产品经理说:还要加蓝牙协议栈、LCD 驱动、按键扫描、电池管理……
我当时的做法是:
- 把 float 系数改成 int16 定点数——体积不变,但运算快了 3 倍
- 把延迟线缓冲区从全局变量改成静态局部变量——节省了 128 字节的全局 RAM
- 把滤波器阶数从 32 降到 16——性能损失 2dB,但 Flash 省了 64 字节,RAM 省了 64 字节
最终产品顺利量产。你看,优化不是非黑即白,而是在约束条件下找最优解。
我曾经踩过的坑:有一次为了省 4 字节 RAM,把一个标志位塞进了结构体的 padding 位。结果代码移植到新编译器后,padding 布局变了,整个模块跑飞。从那以后,我再也不敢在可移植代码里依赖位域布局。省那 4 字节,不值得。
好了,这一章的核心就这些。记住一句话:嵌入式优化的本质,是在有限的资源里,用最合适的代价满足功能需求。后面的章节,我们会一步步深入编译器优化和代码压缩的具体技术。