如果你翻过Linux内核源码,哪怕只是走马观花,大概率会撞见这样一个反复出现的模式:
if (unlikely(condition_that_does_not_happen_much)) {// do thing} else {// do more common thing}这个神秘的unlikely是什么?为什么要专门告诉计算机某件事"不太可能发生"?它到底做了什么?
![]()
答案可能会让你意外:这个简单的宏,用对了能让性能大幅提升,用错了反而会拖慢程序。搞懂它,我们自己的代码也能跟着受益。
unlikely 到底是什么?
它的定义其实是一个极其简单的宏:
#define unlikely(e) __builtin_expect(!!(e), 0)它接收一个表达式 e,连同数字 0 一起传给__builtin_expect。这个内建函数的作用,是给表达式加一个注解,告诉编译器:这个表达式的值,你"预期"它会等于第二个参数——在这里就是 0。
换句话说,我们是在给编译器提供先验信息:程序里哪个分支更常走,哪个分支很少走。有了这个信息,编译器就能提前帮 CPU 设置好分支预测的概率,从而提升性能。
光说理论太抽象,上例子
来看两个完全相同的循环,唯一的区别是一个用了 unlikely,另一个用了它的对立面 likely:
long unlikely_simple(long num_runs) {long total = 0;for (long i = 0; i < num_runs; i++) {if (unlikely(i % 1000 == 0)) {total += 2;} else {total += i + 1;return total;long likely_simple(long num_runs) {long total = 0;for (long i = 0; i < num_runs; i++) {if (likely(i % 1000 == 0)) {total += 2;} else {total += i + 1;return total;}在这两个循环里,if 分支只有 0.1% 的概率被走到,else 分支占了 99.9%。显然,真正"更可能"的路径是 else 分支——但我们完全可以通过选择不同的宏,反过来误导编译器。
把循环里的 1000 改成不同的值,观察不同概率下两个宏的性能表现,数据会说话:
随着走第一个分支的比例越来越低,likely_simple 的编译器提示就越来越不准。我们明明提示编译器往第一个分支优化,但实际执行时程序却频繁走第二个分支——性能自然就变差了。
为什么提示会影响性能?答案在汇编里
数据已经证明,这个提示确实在影响循环的处理方式。但原因是什么?
答案藏在汇编代码里。用gcc -O2 unlikely.c -o bin/unlikely编译后,对比两个循环生成的汇编,就能看到编译器根据我们的提示,对分支指令的排列和预测逻辑做了不同的安排。
CPU 的分支预测器会基于历史执行记录来猜测下一步跳转到哪里。当编译器把"更可能"的分支放在预测器默认会选的位置时,预测命中率就高,流水线不用清空重来;反之,如果提示和实际执行方向相反,预测器频繁猜错,CPU 就要反复丢弃已执行的指令,性能自然就崩了。
给我们的启示
这个宏的价值在于:它让我们能把自己对程序行为的理解,提前告诉编译器。但前提是——你的判断必须准确。
- 如果某个分支确实极少执行,用unlikely是免费的性能优化
- 如果某个分支其实经常执行,却误用了 unlikely,反而会适得其反
- 对于性能关键路径上的分支判断,值得花时间确认真实的分支概率
Linux 内核里到处是这种微小的优化,单个看微不足道,但累积起来,就是内核能在各种硬件上保持高效的原因之一。下次写代码时,不妨想想:这个分支,真的像我想的那样常走吗?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.