在代码里多加一行,程序非但不会变慢,反而能快上四倍。这听起来像都市传说,但 2026 年 7 月 12 日,一位叫 purplesyringa 的程序员在博客上记录了自己亲身验证过的这件事。
他当时在写一个数据压缩程序。里面有一个很短的循环,只有一行核心代码——反复从一个表格里查下一个值,然后把查到的东西存起来。就这么一句话,干净利落。但程序跑起来慢得让人抓狂。他试了各种常规优化,效果都不好。最后他做了一件连他自己都觉得荒谬的事:加了一个看起来完全多余的 if 判断——判断”新查到的值和当前值是不是一样”,如果不一样就更新,一样就跳过。
这行 if 的”废话”程度大概相当于:你已经知道自己口袋里有一百块钱,但还是伸手进去摸一下,确认它真的在,然后才出门。加不加,你口袋里都是一百块。但神奇的是:加上之后,程序从跑 320 微秒变成了跑 80 微秒,整整快了四倍。
笔者第一次读到这个案例时,也觉得像段子。但这不是黑魔法。它背后藏着一个关于现代计算机如何”猜”答案的故事。
工厂流水线上的瓶颈
要理解这件事,得先知道 CPU 是怎么干活的。
把 CPU 想象成一条工厂流水线。流水线上的工人不会等上一件产品完全组装完才开始下一件——那样太慢了。他们会把工作拆成很多小步骤:裁切、打磨、喷漆、质检……每一站同时处理不同的产品。这样一来,整条线的产出速度取决于”最慢的那一站”,而不是”做完一个再做一个”。这就是现代 CPU 的”指令级并行”——同时处理多条指令,大幅提高效率。
但流水线有一个致命弱点:如果下一件产品是什么,取决于上一件产品加工完才知道,那整个流水线就会卡住。工人只能干等。
在 purplesyringa 的代码里,情况就是这样。他的循环是:j = next_j[i][j]——用当前值 j 去查表,查出下一个 j,再把这个新的 j 拿去查下一轮。每一轮都依赖上一轮的结果。CPU 里的流水线工人们焦虑地等着上一站出货,而上一站也在等更上一站……整条线变成了一列单行道的堵车长龙。这就是所谓的”数据依赖链”造成的延迟瓶颈。
一个会”猜路”的导航系统
但现代 CPU 有一项绝活,恰好能对付这种局面。它叫”分支预测器”。
还是用工厂的比喻:流水线上有一个质检站,工人根据检查结果决定产品走 A 通道还是 B 通道。如果他每次都等质检做完再选通道,流水线还是会卡。于是工厂装了一套”历史经验系统”——每当遇到这个质检站,系统就根据过去 99 次的选择来猜:这次大概率还是走 A。工人提前把产品往 A 通道推。如果猜对了,流水线丝滑不停;如果猜错了,就把已经推进 A 通道的半成品拉回来,走 B 通道重新来。
CPU 的分支预测器就是这套系统。它记录了程序过去在每个”岔路口”的选择,然后用一套复杂的电路来预测下一次的走向。现代 CPU 的分支预测准确率通常在 95% 以上——比大多数人类做决策的准确率还高。
purplesyringa 的洞察在于:他的代码里虽然没有明显的”岔路口”(没有 if-else),但数据依赖链本身就是一个隐形的”等待”。他灵光一闪:如果加入一个显式的岔路口,让分支预测器介入呢?
那行”废话”的真正作用
他加的那行 if 是这样的逻辑:判断查表结果和当前值是否不同,如果相同就什么都不做,如果不同才更新。因为绝大多数时候,查出来的值和当前值确实是相同的,所以 CPU 的分支预测器很快”学会”了:这个 if 的身体几乎从来不会被执行。
于是 CPU 大胆猜测:下一轮还是跳过 if 身体。既然猜它跳过,那就不需要等上一轮的结果——直接假设 j 没变,继续往前跑。流水线重新动起来了。多轮循环可以并行处理。
当偶有几次查表结果真的不同时,分支预测器发现自己猜错了,会把已经走错路的半成品清掉,用正确的 j 值重新跑那一轮。这个过程叫”分支预测失败惩罚”。但因为猜错的比例极低,这点代价远小于全程干等的代价。
结果就是:一条看起来完全多余的 if 语句,给了分支预测器一个”可以猜”的信号。它把一条本来只能串行执行的依赖链,变成了一条可以投机并行执行的流水线。
编译器的”好心”办坏事
故事到这里只讲了一半。还有一个更让人头疼的对手:编译器。
编译器是负责把程序员写的人类可读代码翻译成 CPU 能执行的机器指令的程序。现代编译器非常聪明——聪明到它会自动识别”无用代码”并直接删掉。在编译器眼里,purplesyringa 加的那行 if 就是在说”如果 A 不等于 A 才更新 A”,这当然是废话。编译器冷笑一声,把它优化掉了。
程序员想骗过 CPU 的分支预测器,但编译器先一步把骗人的道具没收了。
这就是文章标题里”保守决策”的含义——也是笔者觉得这个案例最有嚼头的地方:编译器严格遵守”不改变程序语义”的原则——你写的东西逻辑上没用,我就不给你翻译。但编译器不知道的是,有些代码的真正价值藏在硬件层面:它给 CPU 提供了一个可以投机执行的信号。
这其实是一场三方博弈。CPU 是激进的:它拼命猜,想方设法把活提前干了。编译器是保守的:它严格遵守语义,不多做也不少做。而程序员站在中间,既想利用 CPU 的激进,又得骗过编译器的保守。
“别碰这里”的封条
purplesyringa 找到的办法是使用 C 语言里一个叫 volatile 的关键字。这个词在 C 语言里相当于给编译器贴了一张”别碰这里”的封条——告诉编译器:这段数据可能在你不知道的情况下被改变,所以不要优化它,每次老老实实读。
贴上这个封条之后,编译器不再认为 if 条件是”永远不成立”的废话,于是保留了下来。if 保住了,分支预测器就有东西可以猜了,流水线就能重新并行运转。
后来在 Lobsters 社区的讨论中,另一位程序员 ibookstein 发现,使用 C++20 的 [[unlikely]] 标注(相当于明确告诉编译器”这个分支很少走”)也能达到类似效果。不过 purplesyringa 指出,volatile 封条方式生成的机器码质量更好,而且不局限在某个特定编译器上。
一个更大的概念:值投机
在 Lobsters 讨论串里,有人指出这个技巧其实有一个正式的名字——“值投机”(value speculation)。核心思路是:当我们对某个值的取值有一个”大概率猜对”的启发式方法时,可以利用分支预测器来投机执行,从而打破数据依赖链。
这个概念最早可以追溯到更早的研究和博客(Paul Khuong、Per Vognsen 等人的工作)。在 mazzo.li 的一篇经典文章中,同样的技巧被用来加速链表遍历:遍历一个链表时,下一个节点的地址取决于当前节点里存的指针,这也是一个数据依赖链。但如果我们猜测”下一个节点就在内存中紧挨着当前节点的位置”,就可以让 CPU 提前预取,把 14GB/s 的吞吐量提升到 45GB/s(当数据在 CPU 缓存中时)。
purplesyringa 的 if 技巧和值投机本质上是同一件事:用廉价的猜测替代昂贵的等待。
什么在跟你作对
这件事最有趣的地方在于,它揭示了三层”你以为 vs 实际”的冲突:
第一层,人类直觉认为”代码越少跑得越快”。但这个案例里,多加一行代码反而更快——因为那行代码的功能是发信号,不是做运算。
第二层,编译器认为”逻辑上无用的代码应该删掉”。但有些代码的用处藏在硬件行为层,不在逻辑语义层。
第三层,我们通常认为”猜错了要付出代价,所以最好别猜”。但现代 CPU 的设计哲学恰恰相反:大胆猜,猜对了就赚,猜错了大不了推倒重来。只要猜对的概率足够高,整体就是赚的。
这个故事没有宏大叙事的结尾。它只是一个程序员在优化一个压缩算法时,偶然撞见的一个反直觉事实。但透过这行小小的 if 语句,你能看见现代计算机底层一个精妙的真相:CPU 是一个赌徒,编译器是一个律师,而最优秀的程序员,往往是懂得何时该骗过律师、把信息递给赌徒的人。
参考链接:
- Purplesyringa 博客:Quadrupling code performance with a “useless” if
- Lobsters 社区讨论
- mazzo.li:Beating the L1 cache with value speculation