分支预测优化: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包裹,编译器会把正常路径排得更紧凑。

小提示:likely/unlikely不会改变程序的逻辑,只影响代码布局。编译器会把likely的分支放在紧挨着条件判断的位置,减少跳转指令。

减少分支:能砍就砍

有些分支其实是可以消除的。我在项目中遇到过一段图像处理代码,里面有个循环,每次都要判断像素值是否在有效范围内:

// 原始代码:每次循环都有分支
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%) 中高

知识体系图

下面这张图展示了分支预测优化的核心逻辑和三种方法的关系:

分支预测优化知识体系 核心问题:分支预测失败 方法一:likely/unlikely 方法二:减少分支 方法三:查表法 告诉编译器分支概率 位运算、条件移动替代if 预计算+数组索引 收益:减少流水线冲刷,提升指令级并行效率

避坑指南

我曾经在一个项目里,把所有if都加上了likely/unlikely,结果性能反而下降了。为什么?因为分支概率接近50%时,告诉编译器「大概率走这边」反而会误导它。

所以,使用这些技巧前,最好先做profiling,确认分支的真实概率。

另外,查表法虽然快,但会占用额外的内存。在嵌入式系统里,256字节的表可能不算什么,但如果是16位的表,那就是64KB了。要掂量着用。

核心总结:分支预测优化的本质,是减少CPU「猜错」的概率。likely/unlikely是给编译器递小抄,减少分支是直接砍掉判断,查表法是把运行时判断变成编译时计算。三种方法各有适用场景,没有银弹。

好了,关于分支预测优化,就聊这么多。记住一点:先测量,再优化。别凭感觉乱加likely,也别为了炫技强行查表。代码清晰、可维护,永远是第一位的。