首页 › programming › 分支预测神课:别让CPU白赌

别让CPU白赌:一堂百万级播放的无分支编程神课

概述

在C++圈,有一类演讲每隔几个月就会被 Hacker News 和 Reddit 的 r/cpp 版块重新翻出来讨论一轮——Fedor Pikus 在 CppCon 2021 大会上的这场《Branchless Programming in C++》就是典型。它讲的是一个非常小、却直插性能工程命门的问题:你的 if 语句,正在让 CPU 赌博,而且赌输了要付出十几级流水线的代价。这场演讲之所以常年被开发者社区惦记,是因为它没有停留在“分支很慢”的口号上,而是把“什么时候分支慢、编译器已经替你做了什么、无分支写法的真实代价是什么”这三件事讲透了。本文按视频的叙事脉络逐层拆解,并把背后的体系结构研究、工程实践和争议点一并补齐——没看过视频,你能看懂这门课;看过视频,你能多懂一层。

视频基本信息

项目 内容
视频标题 Branchless Programming in C++(C++ 无分支编程)
频道 CppCon(C++ 大会官方频道,youtube.com/@CppCon)
演讲者 Fedor Pikus,性能工程专家,著有《The Art of Writing Efficient Programs》
发布时间 2021年 CppCon 大会演讲录像,由官方频道于会后发布(据频道页面)
热度数据 长期居该频道近年演讲播放前列,是开发者社区每逢性能话题必翻出的旧课(据频道页面)
主题定位 编译器优化 × CPU 微架构 × 性能测量方法

一、核心主张拆解:演讲的四层递进

第一层:分支的本质,是让流水线去赌

演讲的开场没有直接讲代码,而是先摆了一个硬件事实:现代 CPU 是深度流水线机器,取指、译码、执行、访存、写回被拆成十几级流水级,每一级都必须时刻有活干,否则吞吐量就会塌方。而分支指令(if、循环的跳转、switch)恰恰是流水线的天敌——在分支条件真正算出来之前,CPU 根本不知道下一条该取哪个地址的指令。

硬件的解法是投机执行(speculative execution):分支预测器先猜一个方向,流水线按猜的结果满负荷跑下去;猜对了,猜的成本约等于零;猜错了,已经流入流水线的指令全部作废、清空重来,这就是所谓分支预测失败惩罚。按 Intel 优化手册和 Agner Fog 微架构资料里业界常用的量级,现代大核上这个惩罚大约在十几到二十个时钟周期。演讲用这个背景立起全片的核心命题:分支本身不贵,赌输了才贵。

第二层:可预测的分支几乎免费,不可预测的分支才要命

这是整场演讲最有认知价值的一层。Pikus 强调,“去掉 if 就能加速”是社区流传最广的误读。现代分支预测器对有规律的模式极其擅长:循环边界、状态机的固定跳转、跟你上一千次行为一致的判断,预测准确率轻轻松松上到 95%–99% 的量级,这类分支写成再“分支化”的代码都不心疼。

真正贵的只有一类:数据相关、模式上接近随机的分支。比如对一串无序数据逐个判断大小、解析器里字符分类这种“上帝都猜不准”的决策点。演讲里那个经典对照实验的思想来自一个著名的 Stack Overflow 提问:同样的循环,对已排序数组跑比对未排序数组跑快好几倍——代码一行没改,唯一的区别是排序让分支变得可预测了。所以正确的目标从来不是“消灭分支”,而是“消灭不可预测的分支”,或者把数据组织得让分支变得可预测。

第三层:编译器早就比你勤快

第三层是给手写“性能代码”的人泼的冷水。Pikus 指出,在开启优化的编译器眼里,很多 if 语句根本活不到汇编那一层。编译器会把简单的条件赋值直接翻译成 x86 的条件传送指令(cmov):两个值都算出来,用条件决定哪个生效,全程没有跳转、不给预测器出题。你写 if (a > b) x = a; else x = b;、写三目运算符、写 std::min/std::max,在汇编层面很可能是同一条 cmov。

但编译器这么做是有前提的,演讲把两个坑都点了出来:一是语义等价——std::min 对 NaN 的行为、条件分支里带副作用(比如自增、IO)的情况,编译器不敢也不会替你转成 cmov;二是溢出与未定义行为会让看似等价的改写悄悄变质。结论是:你以为你在“手动优化”,很多时候只是在跟编译器抢活,还抢输了。

第四层:无分支写法是手艺,但不是玄学

在真正需要消除分支的热点上,演讲给出了一组可复用的模式:

每一招演讲都配了汇编和性能计数器的对照,反复传递同一个方法论:改之前看汇编、改之后看数据,一切以测量为准。C++20 的 [[likely]]/[[unlikely]] 属性、GCC 的 __builtin_expect、基于真实负载数据的 PGO(配置文件引导优化),都被定位为“比手写汇编级技巧更值得优先尝试”的正规军武器。

二、知识与理论溯源:这门课背后的五十年体系结构史

演讲里一句带过的内容,背后是一整条真实的研究脉络,这也是它经得起深挖的原因。

动态分支预测的起点是 James E. Smith 1981 年的经典论文《A Study of Branch Prediction Strategies》,他提出的饱和计数器思想——用两位状态机记录“最近猜对猜错”,错了才降级、对了连对升级——直到今天仍是几乎所有预测器的地基。

两级行进发生在 1991 年:Yeh 与 Patt 提出“两级自适应分支预测”,用“最近 n 次分支的历史模式”索引一张预测表,让预测器学会更长的行为周期;1993 年 McFarling 的 gshare 方案把分支地址与全局历史异或后查表,成为教科书级组合。此后高端处理器进入“锦标赛预测器”时代——Digital 的 Alpha 21264 就同时装了局部历史和全局历史两套预测器,动态择优,这些在 Hennessy 与 Patterson 的《计算机体系结构:量化研究方法》里都有系统记述。

现代王座属于 TAGE:André Seznec 与 Philippe Michaud 在 2006 年提出的几何历史长度标记预测器,用多条不同长度的全局历史互相竞争备份,把预测准确率推到新高。据体系结构文献与公开资料,从 Oracle SPARC 系列开始,TAGE 系设计陆续进入商用高性能核,AMD Zen 一系处理器采用 TAGE 风格的二级预测器也经社区逆向分析与厂商渠道广为流传。换句话说,你在写 if 时面对的,是一台背着一整本“你的代码行为史”在赌博的机器。

赌局的另一面在 2018 年爆发:Kocher 等人的 Spectre 论文与 Lipp 等人的 Meltdown 论文证明,投机执行虽然“失败会回滚”,但它的副作用(缓存状态)会留下来,攻击者可以借这条信道把内核内存读出来。从此“分支预测”不再只是性能话题,也是安全话题——这也是为什么理解这条演讲的机制,对任何一个写系统级代码的人都不算多余。

工程侧的支撑则非常成熟:Linux 的 perf 工具可以直接统计 branch-misses 事件,Intel 优化手册和 Agner Fog 的微架构手册给出了各代处理器的分支失败代价参考值,编译器社区把 cmov 化、likely 提示、PGO 打磨成了日常工具链的一部分。

三、批判性审查:哪些可信,哪些要打折

证据充分的部分:流水线与投机执行的机制描述、预测失败惩罚的量级、cmov 与分支预测的硬件权衡,全部与公开的体系结构文献和厂商手册一致,属于教科书级共识;“先测量再优化”的方法论在性能工程界没有争议。

需要打折扣的部分有三处。

其一,“无分支=快”这个标题式的口号天然容易被夸大。准确表述是:只有消除“数据相关且不可预测”的分支才可能赚。对可预测分支强行无分支化,反而可能更慢——cmov 的代价是两边都算,无论预测对错都要付;如果两个分支体里有一条长依赖链或一次访存,无分支版本的最坏情况好看了,平均情况却输了。演讲本身反复强调这一点,但社区转述时经常只留下“if 慢”这一个字节。

其二,微基准与真实负载的鸿沟。演示用的基准数据往往是特意造出来的“最坏分布”(如均匀随机数),生产代码里这么纯的不可预测分支并不多见;反过来,真实服务的分支分布会随流量漂移,实验室里测得的收益可能上线即蒸发。

其三,汇编层面的结论有时效性。同一份代码,换一版编译器、换一个目标微架构,cmov 化决策就可能翻转。这既是这门课的价值(教你读懂编译器在做什么),也是它的边界(具体结论必须逐版本重验)。性能工程的老规矩依然成立:先算法、再数据布局、最后才是指令级技巧。

四、认知提升与实用启示

第一条:给热点代码装上“分支体检”。 遇到性能问题不要先猜,用 perf stat -e branch-misses,branches 看一眼热点函数的分支失败率。失败率个位数百分比的分支根本不值得动手;只有看到百分之十几以上的失败率,才说明你真的抓到了一个“赌输 ”的热点。

第二条:优先改数据,其次改提示,最后才改写法。 排序、分桶、把偶发事件从热路径剥离,往往一行架构级的改动胜过一百行位运算体操;确定热点后,先试 [[likely]]/[[unlikely]] 和 PGO 这类不动代码结构的手段,最后才考虑手写无分支模式——并保持可读性注释,告诉半年后的自己这段代码为什么长这样。

第三条:把“预测器是个学习者”内化成设计直觉。 你的程序行为越有规律,CPU 替你跑得越快。这意味着:状态机要有稳定的转移模式、热路径上的分支要尽量与数据分布解耦、批量处理要成块成段。写代码时想象有一个学生在一旁记录你的每一步、试图预测你的下一步——别出难题给它。

视频论证结构图

flowchart TD
    A[分支指令进入流水线] --> B{结果可预测吗}
    B -->|有规律| C[预测器命中<br/>流水线全速]
    B -->|接近随机| D[预测失败<br/>清空十几级流水线]
    C --> E[结论一<br/>可预测分支近乎免费]
    D --> F{优化路线怎么选}
    F --> G[无分支写法<br/>cmov与掩码]
    F --> H[编译器提示<br/>likely与PGO]
    F --> I[重排数据<br/>排序分桶成块]
    G --> J[代价<br/>两边都算可读性降]
    H --> K[代价最小<br/>适合绝大多数项目]
    I --> L[让分支重新可预测]
    J --> M[铁律<br/>用perf测量后再动手]
    K --> M
    L --> M

结语

这堂课最大的价值,不在教你几招位运算,而在纠正一个因果:不是“分支慢所以要消灭分支”,而是“预测失败贵,所以要喂给预测器一个可学习的世界”。CPU 每天在你代码上下的几百万次赌注里,绝大多数都赢了——性能工程要处理的,只是它赢不了的那一小撮。看懂这一点,你对 if 的敬畏和对编译器的信任,都会落在该落的位置上。

延伸阅读

  1. Fedor Pikus《The Art of Writing Efficient Programs》(Packt, 2021)— 本场演讲作者的系统化展开,性能工程方法论全覆盖
  2. Wikipedia: Branch predictor — 从饱和计数器到 TAGE 的分支预测器技术脉络

参考来源

  1. CppCon 官方频道 — 演讲视频所在频道,站内搜索 "Branchless Programming in C++" 可直达原视频(据频道页面)
  2. Branch predictor - Wikipedia — 分支预测器历代结构与准确率数据
  3. Spectre (security vulnerability) - Wikipedia — 投机执行副作用演变为安全漏洞的始末
  4. CppCon 官网 — 大会与历年议程档案

本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入