条件分支指令:B.cond、CBZ/CBNZ、TBZ/TBNZ 与延迟槽
条件分支,说白了就是让程序学会「看情况办事」。ARM64 里这块设计得很精巧,既有传统的条件码分支,也有针对零值和特定位测试的专用指令。我最早接触 ARM 时,总觉得指令集里分支种类太多,后来踩过几次坑才明白——每种分支都有它不可替代的场景。
B.cond:条件码分支
B.cond 是 ARM64 里最基础的条件分支。它根据 PSTATE 中的条件标志位(N、Z、C、V)决定是否跳转。指令格式很简单:
B.cond label ; cond 是条件后缀,如 EQ、NE、GT、LT 等
常用的条件后缀有这些:
| 条件后缀 | 含义 | 标志位判断 |
|---|---|---|
| EQ | 相等 | Z == 1 |
| NE | 不相等 | Z == 0 |
| GT | 有符号大于 | Z == 0 && N == V |
| LT | 有符号小于 | N != V |
| HI | 无符号高于 | C == 1 && Z == 0 |
| LS | 无符号低于或相同 | C == 0 || Z == 1 |
我个人习惯把 B.cond 用在循环控制和简单的状态判断上。比如:
loop:
subs w0, w0, #1 ; w0-- 并更新标志位
b.gt loop ; 如果 w0 > 0 则继续循环
这里有个细节:subs 的 's' 后缀会更新标志位,而普通 sub 不会。我见过不少新手在这里翻车——用了 sub 然后发现条件分支永远不触发。
CBZ / CBNZ:比较零值分支
CBZ 和 CBNZ 是 ARM64 的特色指令。它们直接比较寄存器是否为零,然后决定跳转。不需要先执行比较指令,一条指令搞定。
CBZ Xn, label ; 如果 Xn == 0 则跳转
CBNZ Xn, label ; 如果 Xn != 0 则跳转
为什么需要它们?你想想看,如果每次判断零值都要先 CMP 再 B.cond,那至少两条指令。CBZ/CBNZ 把这两步合并了,既省指令又省流水线周期。
不过要注意,CBZ/CBNZ 的跳转范围有限,只有 ±1MB。超出范围就得用其他方式了。
TBZ / TBNZ:测试位分支
TBZ 和 TBNZ 就更专了——它们测试寄存器中特定位的值。这在处理状态寄存器、标志位或位图时非常方便。
TBZ Xn, #bit, label ; 如果 Xn 的第 bit 位为 0 则跳转
TBNZ Xn, #bit, label ; 如果 Xn 的第 bit 位为 1 则跳转
举个例子,假设你有一个状态寄存器,第 3 位表示「数据就绪」:
wait_data:
ldr x0, [x1, #STATUS_OFFSET] ; 读取状态寄存器
tbz x0, #3, wait_data ; 如果第3位为0,继续等待
ldr x2, [x1, #DATA_OFFSET] ; 读取数据
我曾经在调试一个外设驱动时,发现中断响应总是慢一拍。查了半天,原来是用了 AND + CBNZ 的组合来测试位,改成 TBNZ 后延迟直接降了 30%。
条件分支的延迟槽与预测机制
这部分是性能调优的关键。ARM64 没有像早期 MIPS 那样的显式延迟槽,但分支预测的代价依然存在。
现代 ARM 处理器使用动态分支预测器,它会记录每条分支指令的历史行为。预测正确时,流水线几乎无损失;预测错误时,需要清空流水线并重新取指,代价大约是 10-15 个周期。
这里有几个实用建议:
- 让分支可预测:尽量让同一分支指令每次都走同一方向。比如循环的向后跳转几乎总是「跳」,预测器很快就能学会。
- 避免数据依赖的分支:如果分支条件来自随机数据(比如哈希表查找),预测器基本失效。这时考虑用条件指令或无分支算法。
- 利用条件指令:ARM64 提供了 CSEL、CSINC、CSINV 等条件选择指令,它们不改变程序流,没有预测惩罚。
举个例子,下面这段代码:
; 分支版本
cmp w0, #10
b.gt greater
mov w1, #1
b end
greater:
mov w1, #2
end:
可以改写成无分支版本:
; 无分支版本
cmp w0, #10
csel w1, w2, w1, gt ; 如果 w0 > 10,w1 = w2,否则 w1 = w1
后者没有分支,流水线永远不 stall。在性能敏感的热路径上,这种优化效果非常明显。
知识结构总览
下面这张图总结了本章的核心知识点和它们之间的关系:
核心要点:
- B.cond 通用性强,但需要额外指令设置标志位
- CBZ/CBNZ 适合判空场景,一条指令搞定
- TBZ/TBNZ 专治位测试,比 AND+CBNZ 组合高效
- 分支预测错误代价高,热路径上尽量用条件指令替代
嗯,到这里条件分支的核心内容就差不多了。记住一点:没有银弹。每种分支指令都有自己的舞台,选对了事半功倍,选错了性能翻车。我建议你在写代码时多想想——这个分支真的需要吗?能不能用条件指令替代?