条件分支指令: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 把这两步合并了,既省指令又省流水线周期。

我的经验:在遍历链表或数组时,CBNZ 特别好用。比如 while(ptr != NULL) 这种循环,用 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 个周期。

避坑指南:我曾经在视频编解码项目中,因为一个频繁翻转的分支(比如交替执行 if-else),导致性能暴跌 40%。后来用条件选择指令 CSEL 替代了分支,问题才解决。

这里有几个实用建议:

  • 让分支可预测:尽量让同一分支指令每次都走同一方向。比如循环的向后跳转几乎总是「跳」,预测器很快就能学会。
  • 避免数据依赖的分支:如果分支条件来自随机数据(比如哈希表查找),预测器基本失效。这时考虑用条件指令或无分支算法。
  • 利用条件指令: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。在性能敏感的热路径上,这种优化效果非常明显。

知识结构总览

下面这张图总结了本章的核心知识点和它们之间的关系:

ARM64 条件分支指令知识结构 ARM64 条件分支指令体系 B.cond 条件码分支 CBZ / CBNZ 零值分支 TBZ / TBNZ 位测试分支 依赖 PSTATE 标志位 需先执行 CMP / SUBS 等 跳转范围 ±1MB 单指令完成比较+跳转 仅支持零值判断 跳转范围 ±1MB 测试特定位的值 位索引范围 0-63 跳转范围 ±1MB 分支预测与延迟槽优化 预测错误代价 10-15 周期 → 优先使用 CSEL 等条件指令

核心要点:

  • B.cond 通用性强,但需要额外指令设置标志位
  • CBZ/CBNZ 适合判空场景,一条指令搞定
  • TBZ/TBNZ 专治位测试,比 AND+CBNZ 组合高效
  • 分支预测错误代价高,热路径上尽量用条件指令替代

嗯,到这里条件分支的核心内容就差不多了。记住一点:没有银弹。每种分支指令都有自己的舞台,选对了事半功倍,选错了性能翻车。我建议你在写代码时多想想——这个分支真的需要吗?能不能用条件指令替代?