一、性能调优概述:为什么要做性能调优?
说实话,我刚开始做嵌入式开发那几年,对性能调优这事儿挺不屑的。
那时候我觉得,代码能跑、功能正常,不就完了吗?干嘛非要跟自己过不去,去抠那几个时钟周期、那几字节内存?
直到有一次,我负责的一个数据采集项目,在实验室里跑得好好的,一上现场就崩了。原因很简单——数据量一上来,CPU 扛不住了,中断响应延迟从几十微秒飙到了几毫秒,采集的数据全乱了。那次我被客户骂得狗血淋头,连夜改代码,把缓冲区从链表改成环形队列,把浮点运算换成定点运算,折腾了三天三夜才稳住。
从那以后,我彻底明白了:性能调优不是锦上添花,而是雪中送炭。
1.1 为什么非要做性能调优?
你想想看,一个嵌入式系统,资源就那么点。CPU 主频可能只有几十兆赫兹,内存可能只有几百 KB,Flash 更是抠着用。在这种环境下,代码写得糙一点,系统就可能在关键时刻掉链子。
我总结了几条必须做性能调优的理由:
- 成本控制:同样的功能,代码优化好了,就能用更便宜的芯片。我见过一个项目,优化前必须用 Cortex-M4,优化后 M0 就搞定了,一颗芯片省了 2 块钱,一年出货 10 万片,你算算省了多少?
- 实时性保障:工业控制、汽车电子这些领域,响应慢了就是事故。我有个朋友做电机控制,中断响应延迟超过 100 微秒,电机就抖得跟筛子似的。
- 功耗管理:电池供电的设备,CPU 跑得快就能早点睡,睡得多就省电。优化好的代码,能让系统在 1 毫秒内干完活然后进入休眠,而不是磨磨蹭蹭跑 10 毫秒。
- 可靠性提升:内存用得少,就越不容易溢出;堆栈分配合理,就越不容易踩踏。说白了,优化过的代码,往往也更健壮。
核心观点:性能调优的本质,是用更少的资源做更多的事。在嵌入式世界里,资源就是钱,就是电,就是可靠性。
1.2 性能指标有哪些?
做调优之前,你得先知道要优化什么。就像看病,得先量体温、测血压,不能上来就开药。我常用的性能指标有五个:CPU 利用率、内存占用、IO 吞吐量、延迟、吞吐量。
下面这张图,是我自己画的一个性能指标关系图,帮你理清它们之间的联系:
这五个指标,我习惯把它们分成两类:资源类和性能类。CPU、内存、IO 属于资源类,你用了多少资源;延迟和吞吐量属于性能类,你干得有多快、能干多少。
1.3 每个指标怎么定义和测量?
光知道名字没用,你得知道怎么测。我踩过的坑不少,下面一个一个说。
CPU 利用率
说白了,就是 CPU 有多忙。在裸机系统里,我通常用一个定时器中断,在主循环里跑一个空闲任务,统计空闲时间占比。公式很简单:
// 伪代码:测量 CPU 利用率
volatile uint32_t idle_count = 0;
volatile uint32_t total_count = 0;
void timer_isr() {
total_count++;
}
void idle_task() {
idle_count++;
}
void main() {
// 运行一段时间后
float cpu_usage = 100.0f * (1.0f - (float)idle_count / (float)total_count);
printf("CPU 利用率: %.1f%%\n", cpu_usage);
}
我的经验:在 RTOS 环境下,很多系统都提供了现成的 API,比如 FreeRTOS 的 uxTaskGetSystemState()。但要注意,测量本身也会消耗 CPU,所以采样周期别太短,我一般设 100ms 左右。
内存占用
内存分两块:静态内存和动态内存。静态内存看编译器的 map 文件就行,动态内存就得靠工具了。
我常用的方法:
- 查看 map 文件:看 .bss 和 .data 段的大小,这就是静态内存占用。
- 堆栈监测:在任务栈里填充 0xDEAD 或 0xAA,运行一段时间后检查水位线。我曾经用这招抓到一个栈溢出 bug,那个任务实际用了 800 字节,我只给了 512 字节,难怪老死机。
- 动态内存跟踪:自己写一个 malloc 的包装函数,记录每次分配和释放的地址和大小。
// 动态内存跟踪示例
void* my_malloc(size_t size, const char* file, int line) {
void* ptr = malloc(size);
// 记录分配信息到日志
printf("[MEM] %s:%d malloc(%zu) = %p\n", file, line, size, ptr);
return ptr;
}
#define malloc(s) my_malloc(s, __FILE__, __LINE__)
注意:嵌入式系统里,我强烈建议避免动态内存分配。除非你用的是带 MMU 的 Linux 系统,否则 malloc/free 容易导致碎片,而且不可预测。我见过一个产品,运行 72 小时后突然死机,查了三天,发现是内存碎片导致 malloc 返回 NULL。
IO 吞吐量
IO 吞吐量,就是单位时间内能读写多少数据。比如 SPI 通信,你配置了 10MHz 时钟,但实际有效吞吐量可能只有 5Mbps,因为协议开销、中断处理、数据拷贝都在吃时间。
测量方法:
// 测量 SPI 吞吐量
uint32_t start_time = get_tick();
for (int i = 0; i < 1000; i++) {
spi_transfer(buffer, 1024); // 每次传输 1024 字节
}
uint32_t end_time = get_tick();
uint32_t elapsed_ms = end_time - start_time;
float throughput = (1000.0f * 1024.0f * 8.0f) / (elapsed_ms / 1000.0f) / 1000000.0f;
printf("SPI 吞吐量: %.2f Mbps\n", throughput);
延迟
延迟,就是一件事从发起到完成花了多长时间。在嵌入式里,我最关心的是中断响应延迟和任务切换延迟。
测量中断延迟,我习惯用 GPIO 翻转法:
// 测量中断响应延迟
// 在中断触发前,拉高一个 GPIO
GPIO_Set(PIN_TEST, HIGH);
// 中断服务函数里,立即拉低
void EXTI_ISR() {
GPIO_Set(PIN_TEST, LOW);
// ... 处理中断
}
// 用示波器看 GPIO 高电平的宽度,就是中断响应延迟
嗯,这个方法很土,但很有效。我见过有人用逻辑分析仪抓,精度能到纳秒级。
吞吐量
吞吐量和延迟是两码事。延迟是「慢不慢」,吞吐量是「多不多」。比如一个串口,延迟可能只有 1ms,但吞吐量只有 115200bps,因为波特率就那么大。
测量吞吐量,核心就是计数:
// 测量任务处理吞吐量
volatile uint32_t processed_count = 0;
void task_handler() {
while (1) {
process_one_packet();
processed_count++;
}
}
// 每秒打印一次
void timer_report() {
printf("吞吐量: %u 包/秒\n", processed_count);
processed_count = 0;
}
关键认知:延迟和吞吐量往往互相制约。你让系统处理得更快(降低延迟),可能因为频繁切换上下文而降低吞吐量。反过来,你让系统批量处理(提高吞吐量),单个请求的延迟就会增加。这就是经典的「延迟-吞吐量权衡」,我在做网络协议栈优化时深有体会。
1.4 测量工具怎么选?
工具这东西,够用就行。我列个表,你根据自己情况选:
| 指标 | 推荐工具/方法 | 适用场景 |
|---|---|---|
| CPU 利用率 | 定时器 + 空闲计数 / RTOS API | 裸机、FreeRTOS、uC/OS |
| 内存占用 | Map 文件 / 堆栈填充 / 包装 malloc | 所有嵌入式平台 |
| IO 吞吐量 | 示波器 / 逻辑分析仪 / 软件计时 | SPI、I2C、UART、CAN |
| 延迟 | GPIO 翻转 + 示波器 / 定时器捕获 | 中断响应、任务切换 |
| 吞吐量 | 软件计数 / 性能计数器 (PMU) | 数据处理、协议栈 |
一个小建议:别一上来就上高大上的工具。我刚开始做优化时,一个示波器加一个 GPIO 就搞定了 80% 的问题。工具越复杂,引入的干扰越多,有时候反而看不清问题本质。
1.5 什么时候该做性能调优?
这个问题,我自己的原则是:不要过早优化,也不要从不优化。
具体来说:
- 架构设计阶段:考虑性能边界,比如选什么 MCU、用多少内存、通信带宽够不够。这时候不做优化,后面改架构成本太高。
- 功能实现阶段:别纠结微优化,先把功能跑通。我见过有人为了省 4 字节,把代码写得跟天书一样,结果一调试就崩。
- 功能稳定后:用 profiler 跑一遍,找到热点,然后针对性优化。这时候才是真正做性能调优的时候。
我曾经犯过的错:有一个项目,我在功能还没跑通的时候就开始优化循环展开、内联汇编,结果代码改得面目全非,最后发现瓶颈根本不在那里——是 DMA 配置错了,导致每次传输都多等了 100 微秒。白白浪费了两周时间。
所以,记住一句话:先测量,再优化;先找瓶颈,再动手。
好了,这一章我们聊了为什么要做性能调优,以及五个核心指标的定义和测量方法。这些东西,说白了就是给你一把尺子,让你知道自己的系统到底几斤几两。下一章,我们会深入具体的优化技术,从代码层面开始动手。