5.3 控制流指令:分支、跳转与预测


5.3 控制流指令:分支、跳转与预测

本节摘要:控制流指令改写程序计数器,是流水线上最昂贵的常客。本节讲它的三件事:语义(条件从哪来——标志位派与比较派,2.2 节埋的线在此收线)、预测(方向与目标怎么猜、猜错多贵)、以及函数调用这套"结构化跳转"的完整机制(调用约定与栈帧)。读完你应当能解释为什么预测器是现代处理器最聪明的部件,以及一个函数调用在指令层到底发生了什么。

转弯指令的特殊待遇

控制流指令分四型。无条件跳转(x86 的 jmp、RISC-V 的 jal)直改程序计数器;条件分支先判条件再决定改不改(beq、jz 一族);函数调用在跳转之外还要把返回地址存起来(call 把地址压栈或存进 ra 寄存器);函数返回从存储处取回地址跳过去(ret)。前三型算"已知去哪"或"能猜去哪",最麻烦的是间接跳转——目标在寄存器里(函数指针、switch 跳转表、虚函数调用),连静态分析都定不下目标,预测难度最高。

条件的来源是 2.2 节埋过的两派分野。x86 是标志位派:每条算术指令顺带更新标志寄存器(零标志、符号标志、溢出标志、进位标志),条件跳转查表消费:

; x86:算术顺带立标志,跳转只读标志 sub eax, ebx ; 减法立下 ZF/SF/OF/CF jz equal ; 零标志成立则相等

RISC-V 是比较派:没有标志寄存器,比较指令把结果(0 或 1)写进普通寄存器,分支指令直接对寄存器判真:

# RISC-V:比较成结果,分支直接判 sub t0, a0, a1 # 差值进寄存器 beq t0, zero, equal # 等于零即相等 # 或一步到位:beq a0, a1, equal

标志位派省指令(算术与判跳之间无需显式比较),但标志是全核共享状态,乱序机器要花大力气处理多条在途指令同时改标志的"猜名字"问题;比较派多写指令,却让条件成为普通数据,乱序执行畅通无阻。ARM 走过第三条路——每条指令都带 4 位条件码字段(3.2 节的地层之一),无条件执行的指令也能挂条件,分支稀少的规整代码受益,这条设计在 AArch64 被收敛成常规的比较加分支。

预测:流水线的经济命脉

控制冒险的账要算清楚:分支的答案要到执行工位才揭晓,而前端早已跑出去十几条指令。猜对了,等待被完全掩盖;猜错了,错误路径上的全部在途指令作废冲刷,流水线重灌——在现代深流水与宽发射的核心上,一次冲刷的代价是十几个周期,若分支每五条指令就有一条,猜错率哪怕只有一成,性能也要腰斩。预测器因此成为处理器里最像人工智能的部件。

预测分两问:方向(跳不跳)与目标(跳到哪)。方向预测从朴素起步——两位饱和计数器(跳两次才改主意,抗噪声),到用全局历史与局部历史索引的模式表(记住"上几次的走向组合下,这次通常怎么跳"),再到 Tournament 混合、神经预测器,主流核心的方向预测准确率在常规负载上能到 95% 以上。目标预测靠分支目标缓冲(BTB):按分支指令地址缓存上一次的目标地址,取指阶段直接查表改写程序计数器;返回指令另有专属的返回栈缓冲(RSB),按调用的后进先出规律猜返回点——函数返回这种"间接跳转"被结构性规律驯服了一半。间接跳转中最难的虚调用与跳转表,仍是预测错误的重灾区,也是性能剖析里"前端受限"的老主顾。

; 一段求和循环:预测器的日常考场 sum_loop: lw t1, 0(a0) # 取元素 add a1, a1, t1 # 累加 addi a0, a0, 4 # 前移 addi t2, t2, -1 # 计数 bnez t2, sum_loop # 循环分支:N-1 次跳、1 次不跳

循环分支是预测器的送分题:后向分支默认跳,N 次迭代只有最后一次猜错——这是两位计数器的舒适区。它同时示范了循环结构对预测的天然友好:可预测的控制流,是 2.3 节"负载可预测性"在微架构层的具体含义。

函数调用:结构化跳转的全套礼仪

call 与 ret 是被软件纪律驯化的跳转。一次调用在指令层的完整礼仪:调用者按调用约定把参数放进指定寄存器(x86-64 System V 放 rdi、rsi、rdx 等;RISC-V 放 a0 到 a7),执行 call 把返回地址交给 ra(寄存器派)或压栈(栈派);被调者若要自用这些寄存器,先把它们的旧值压进栈帧——函数序言 addi sp,sp,-16 加存寄存器,你在 3.4 节亲手编码过——干完活在尾声恢复、执行 ret。谁保存谁恢复的分工写在调用约定里:调用者保存寄存器(临时值)调用者自理,被调用者保存寄存器(要跨调用的值)由被调者负责压弹。

⚠️ 调用约定不是 ISA 本体,而是平台标准——同一颗 x86-64 芯片上,Windows 与 Linux 的参数寄存器都不同。违反约定的典型故障:内联汇编改了被调用者保存寄存器却不恢复,函数返回后调用者的变量"莫名"被改——这类幽灵缺陷的排查入口就在本节。

栈帧同时是 5.4 节与第 7 章的重要伏笔:异常与中断要靠栈保存现场,安全扩展要靠防护机制看住返回地址(7.4 节的指针认证防的正是返回地址被篡改)。

预测失败的现场重建

算一笔猜错的账,建立代价直觉。设流水线前端深度为 12 级、每拍发射 4 条:一次猜错要冲刷的错误路径指令最多 48 条,折算的周期代价十余拍。若程序每五条指令一条分支、预测准确率 95%,每百条指令产生一条猜错,性能折损约一到两成;准确率掉到 90%,折损翻倍。这笔账解释了预测器军备竞赛的全部动机——预测率每提升一个百分点,都是真金白银的吞吐。它也解释了"分支消除"类优化的含金量:循环展开把 N 次判断并成一次、条件传送把短分支变成无分支数据选择,都是在给预测器减负。

现场重建一个典型事故:某热函数在剖析工具里显示"分支预测失败率异常偏高",代码逻辑却看不出名堂。排查路径照本节知识走:先看分支类型——间接跳转(函数指针、跳转表)是高发区;再看是否多态调用(虚函数的目标随对象类型变化,BTB 学不过来);再看是否分支体量超过预测器历史表容量(超多分支的热函数互相踢场子)。三种病因三种药方:减少间接层、摊平分支密度、或接受现状换数据布局。诊断流程本身就是本章知识的考核卷。

本节要点回顾

  • 控制流四型:无条件跳转、条件分支、调用、返回;间接跳转(目标在寄存器)是预测难度之冠。
  • 条件两派:标志位派(x86)省指令但共享状态难乱序;比较派(RISC-V)把条件变成普通数据;ARM 的每指令条件码是第三条路。
  • 预测分方向与目标两问:两位计数器、历史索引模式表管方向,BTB 与 RSB 管目标;一次猜错代价十几个周期。
  • 循环是预测器的舒适区:可预测控制流是性能的第一生产力。
  • 调用约定是平台标准不是 ISA 本体:参数寄存器、保存分工、栈帧礼仪——违反它产生幽灵缺陷。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U