分支预测优化:likely/unlikely宏、减少分支、查表法替代分支
分支预测,这词听着挺唬人。说白了就是CPU猜你下一步要跳哪去。
我刚开始写嵌入式代码时,压根没想过这回事。直到有一次,一个跑在ARM上的控制程序,性能死活上不去。用示波器一量,发现CPU大部分时间都在「猜错」——流水线刷了又刷,效率低得可怜。
嗯,从那以后,我开始认真对待分支预测了。
为什么分支会拖慢速度?
现代CPU都是流水线架构。一条指令在执行,下一条已经在解码,再下一条已经在取指了。一旦遇到分支指令,CPU就得猜:到底跳还是不跳?
猜对了,万事大吉。猜错了呢?流水线里预取的那些指令全得作废,重新加载正确的路径。这一下,十几个时钟周期就没了。
你想想看,如果一段热代码里频繁出现分支预测失败,性能损失是相当可观的。
likely/unlikely宏:给编译器递小抄
Linux内核里大量使用了这两个宏。它们的作用是告诉编译器:这个分支大概率会走哪条路。
// 定义方式(GCC内置)
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
// 使用示例
if (likely(ptr != NULL)) {
// 大概率执行这里
process_data(ptr);
} else {
// 极少执行这里
handle_error();
}
我个人习惯在错误检查、边界条件这些「几乎不会触发」的地方用unlikely。比如内存分配失败、文件打开失败,这些路径用unlikely包裹,编译器会把正常路径排得更紧凑。
减少分支:能砍就砍
有些分支其实是可以消除的。我在项目中遇到过一段图像处理代码,里面有个循环,每次都要判断像素值是否在有效范围内:
// 原始代码:每次循环都有分支
for (int i = 0; i < N; i++) {
if (pixel[i] > 255) pixel[i] = 255;
if (pixel[i] < 0) pixel[i] = 0;
}
改成无分支版本后,速度提升明显:
// 优化后:无分支,使用位运算
for (int i = 0; i < N; i++) {
pixel[i] = (pixel[i] > 255) ? 255 : pixel[i];
pixel[i] = (pixel[i] < 0) ? 0 : pixel[i];
// 或者用更极端的位运算
pixel[i] = ((255 - pixel[i]) >> 31) & pixel[i] |
((pixel[i] - 0) >> 31) & 255;
}
为什么会这样?因为条件运算符在某些架构上会被编译成条件移动指令(CMOV),不产生分支。而位运算版本更是彻底消灭了分支。
查表法:用空间换时间
查表法是我最喜欢的分支替代方案。说白了,就是把判断逻辑提前算好,存到数组里,运行时直接取结果。
我曾经写过一个协议解析模块,里面要根据字节值做几十种不同的处理。最初用switch-case,代码又长又慢。后来改成查表法:
// 定义函数指针表
typedef void (*handler_t)(uint8_t data);
handler_t handlers[256];
// 初始化表(一次性)
void init_handlers() {
for (int i = 0; i < 256; i++) {
if (i >= 0x00 && i <= 0x1F)
handlers[i] = handle_control;
else if (i >= 0x20 && i <= 0x7E)
handlers[i] = handle_printable;
else
handlers[i] = handle_extended;
}
}
// 运行时:无分支
void process_byte(uint8_t byte) {
handlers[byte](byte); // 直接查表调用
}
你看,运行时完全没有了if-else链,也没有switch-case。一次数组索引,一次函数调用,完事。
三种方法的对比
| 方法 | 适用场景 | 性能收益 | 代码复杂度 |
|---|---|---|---|
| likely/unlikely | 分支概率极不均衡 | 中等(5%~15%) | 低 |
| 减少分支 | 简单条件判断、循环内 | 高(10%~30%) | 中 |
| 查表法 | 多分支、状态机、协议解析 | 高(20%~50%) | 中高 |
知识体系图
下面这张图展示了分支预测优化的核心逻辑和三种方法的关系:
避坑指南
我曾经在一个项目里,把所有if都加上了likely/unlikely,结果性能反而下降了。为什么?因为分支概率接近50%时,告诉编译器「大概率走这边」反而会误导它。
所以,使用这些技巧前,最好先做profiling,确认分支的真实概率。
另外,查表法虽然快,但会占用额外的内存。在嵌入式系统里,256字节的表可能不算什么,但如果是16位的表,那就是64KB了。要掂量着用。
好了,关于分支预测优化,就聊这么多。记住一点:先测量,再优化。别凭感觉乱加likely,也别为了炫技强行查表。代码清晰、可维护,永远是第一位的。