同样一组数字,只是提前排好序,处理速度就能快出6倍。这不是玄学,是CPU里一条叫分支预测的流水线在起作用。
很多人听过"CPU会猜if往哪边走,猜错就变慢"这个说法,但真正的工程谜题没被解开:CPU为什么非得猜?硬件靠什么记住过去的分支?猜错的那一瞬间,芯片里到底发生了什么?为什么一次猜错要毁掉15到20个时钟周期的工作?
![]()
把if语句底下那层硅片机制拆开看,答案就清楚了。
1.89秒和11.42秒的差距
先看一段标准的C基准测试。分配一个含32768个随机整数的数组,数值在0到255之间,然后循环10万次,把大于等于128的值累加进一个求和变量。
用gcc -O2编译后,结果分成两种:
- 开启std::sort排序:约1.89秒
- 不排序、保持随机数组:约11.42秒
循环做的算术运算完全一样,处理的整数集合也完全一样,但未排序的版本慢了600%。
用Linux perf给两次运行做性能剖析,硬件性能计数器直接给出了证据。未排序数组这边,耗时11.42秒,跑了38820114920个周期,指令数26400180410,每周期指令数(IPC)只有0.68,分支数3276800000,分支预测失败1638104210次,占全部分支的49.99%。
排序数组这边,耗时1.89秒,周期数6425890120,指令数23910240000,IPC达到3.72,分支数同样是3276800000,但分支预测失败只有328140次,占比0.01%。
未排序时,CPU有一半的分支预测是错的,49.99%这个数字,基本等于抛硬币。吞吐量从每周期3.72条指令塌到0.68条。要理解原因,得看指令流水线。
CPU为什么必须猜
现代CPU核心,比如Intel Raptor Lake、AMD Zen 4/Zen 5、Apple M系列,不是那种执行完一条指令再取吓一条的解释器。它是一座深度流水线、乱序、超标量的工厂,有14到20多个独立阶段。
取指阶段用程序计数器从L1指令缓存里读16到64个原始字节;译码阶段把变长的x86机器码翻译成固定长度的内部微操作;寄存器重命名把架构寄存器映射到几百个物理推测寄存器,消除假数据依赖;分发和重排序缓冲区把微操作放进保留站,等输入就绪;执行阶段由多个并行ALU、向量单元和加载/存储流水线乱序算出结果;写回和退休阶段再严格按程序顺序把结果提交到架构状态。
一个6宽的核心跑在4GHz,取指引擎每四分之一纳秒就得往流水线里塞6条新指令。
这时候遇到一个条件分支:比较eax和128,大于等于就跳转。条件取决于eax里的值,而eax可能正在从L1缓存加载,或者在等前一次计算。ALU要到第14或15阶段才知道跳转的真实结果。
如果CPU停下来等ALU算完,每个分支都会让流水线卡住15到20个时钟周期。分支大约占典型编译代码的15%到20%,也就是每5到7条指令就有一个分支,等分支会让CPU吞吐量下降超过75%。
CPU等不起。它必须在第1阶段就预测结果,然后沿着假设的路径推测性地取指。
硬件怎么猜
硬件分支预测发生在专用硅片单元里,和取指同时进行。
分支目标缓冲区(BTB)是一块按程序计数器低位索引的专用缓存。CPU还没译码出刚取到的指令是什么,就得先知道两件事:这条指令是不是分支?如果跳转,目标地址在哪?分支执行时,BTB会存下它的目标地址,之后再次取到同一个程序计数器,BTB能在0到1个时钟周期内给出目标地址。
预测条件分支是跳转还是不跳转,早期CPU用1位历史标志。1位预测的问题出在循环退出惩罚上:一个跑1000次的循环,1位预测器会错两次,一次是循环终止时,一次是……
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
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.