条件执行与分支优化:B.cond、CSEL、CCMP
条件执行,说白了就是让CPU根据某个条件来决定下一步干什么。很多刚接触ARM64的朋友会觉得这玩意儿不就是个if-else吗?其实没那么简单。我最早在x86上做优化时,条件跳转用得很随意,后来转到ARM64平台,才发现这里面的门道深得很。
ARM64的条件执行机制,核心就三条指令:B.cond(条件跳转)、CSEL(条件选择)、CCMP(条件比较)。这三兄弟配合好了,能让你的代码跑得飞快;用不好,流水线一停,性能直接腰斩。
核心观点:条件分支的代价远比你想象的大。一次分支预测失败,代价是20-40个时钟周期。而条件选择指令(CSEL)和条件比较指令(CCMP)可以完全避免分支,代价只有1-2个周期。
3.1 条件跳转指令 B.cond
B.cond 是最直观的条件执行方式。它根据 PSTATE 中的条件标志位(NZCV)来决定是否跳转。ARM64 提供了16种条件码,我列个表给你看:
| 条件码 | 含义 | 标志位条件 | 等价C语言 |
|---|---|---|---|
| EQ | 等于 | Z == 1 | == |
| NE | 不等于 | Z == 0 | != |
| CS/HS | 无符号大于等于 | C == 1 | >= (unsigned) |
| CC/LO | 无符号小于 | C == 0 | < (unsigned) |
| MI | 负数 | N == 1 | < 0 |
| PL | 非负数 | N == 0 | >= 0 |
| VS | 溢出 | V == 1 | 溢出 |
| VC | 无溢出 | V == 0 | 无溢出 |
| HI | 无符号大于 | C == 1 && Z == 0 | > (unsigned) |
| LS | 无符号小于等于 | C == 0 || Z == 1 | <= (unsigned) |
| GE | 有符号大于等于 | N == V | >= (signed) |
| LT | 有符号小于 | N != V | < (signed) |
| GT | 有符号大于 | Z == 0 && N == V | > (signed) |
| LE | 有符号小于等于 | Z == 1 || N != V | <= (signed) |
| AL | 无条件 | 总是 | 无条件 |
| NV | 从不 | 从不 | 不可用 |
嗯,这里要注意:AL 和 NV 在 ARM64 中基本不用。AL 就是无条件跳转,直接用 B 就行;NV 是保留编码,用了会触发未定义指令异常。
举个例子,一个简单的 if-else:
// C 代码
if (x > 0) {
y = 1;
} else {
y = 2;
}
// ARM64 汇编
cmp x0, #0 // 比较 x0 和 0
b.le .L_else // 如果 x0 <= 0,跳转到 else
mov x1, #1 // then 分支:y = 1
b .L_end
.L_else:
mov x1, #2 // else 分支:y = 2
.L_end:
这段代码看着没问题,对吧?但我在项目中遇到过一个问题:当 x0 的值随机分布时,分支预测器经常猜错,性能惨不忍睹。后来我换成了 CSEL,效果立竿见影。
避坑指南:我曾经在一个网络包处理模块里大量使用 B.cond,结果吞吐量死活上不去。用 perf 一查,分支预测失败率高达 15%。换成 CSEL 后,预测失败率降到接近 0,吞吐量提升了 30%。
所以我的建议是:能用 CSEL 的地方,就别用 B.cond。
3.2 条件选择指令 CSEL
CSEL 是 ARM64 的明星指令。它根据条件码从两个源寄存器中选择一个写入目标寄存器。没有分支,没有跳转,没有流水线冲刷。
指令格式:
CSEL Xd, Xn, Xm, cond
// 如果 cond 为真,Xd = Xn;否则 Xd = Xm
刚才那个 if-else 用 CSEL 改写:
cmp x0, #0 // 比较 x0 和 0
csel x1, xzr, x2, gt // 如果 x0 > 0,x1 = 0;否则 x1 = x2
// 等等,这里逻辑不对。我们想要的是 y = (x > 0) ? 1 : 2
// 正确写法:
mov x2, #1
mov x3, #2
cmp x0, #0
csel x1, x2, x3, gt // 如果 x0 > 0,x1 = 1;否则 x1 = 2
你想想看,这段代码没有分支,CPU 可以一直流水线执行,效率高得多。
CSEL 还有几个变种:
- CSINC:条件选择并加1。如果条件为真,返回 Xn;否则返回 Xm+1。
- CSINV:条件选择并取反。如果条件为真,返回 Xn;否则返回 ~Xm。
- CSNEG:条件选择并取负。如果条件为真,返回 Xn;否则返回 -Xm。
这些变种在实现绝对值、饱和运算时特别有用。比如求绝对值:
// 求 x0 的绝对值,结果存到 x1
// 如果 x0 >= 0,返回 x0;否则返回 -x0
cmp x0, #0
csneg x1, x0, x0, ge // 如果 x0 >= 0,x1 = x0;否则 x1 = -x0
一条指令搞定,没有分支,没有跳转。这就是 ARM64 的魅力。
小技巧:CSEL 系列指令的延迟只有 1-2 个周期,而一次分支预测失败要损失 20+ 个周期。所以,任何简单的条件赋值都可以用 CSEL 替代。但要注意,如果条件分支内部有复杂的计算或函数调用,CSEL 就不合适了。
3.3 条件比较指令 CCMP
CCMP 是个很有意思的指令。它允许你在条件满足时才执行比较操作,否则设置一个默认的标志位值。这玩意儿在实现复杂条件判断时特别有用。
指令格式:
CCMP Xn, Xm, #nzcv, cond
// 如果 cond 为真,比较 Xn 和 Xm,更新 NZCV
// 否则,将 NZCV 设置为 #nzcv 指定的值
为什么要用 CCMP?我举个例子。假设你要判断一个数是否在 [10, 20] 范围内:
// C 代码
if (x >= 10 && x <= 20) {
// do something
}
// 传统汇编写法
cmp x0, #10
b.lt .L_out // 如果 x < 10,跳出去
cmp x0, #20
b.gt .L_out // 如果 x > 20,跳出去
// 在范围内,执行操作
.L_out:
// 使用 CCMP 的写法
cmp x0, #10 // 第一次比较:x >= 10?
ccmp x0, #20, #4, ge // 如果 x >= 10,再比较 x <= 20
// 否则,设置 Z=0, C=0, V=0, N=0 (nzcv=0100)
b.gt .L_out // 如果最终结果 x > 20,跳出去
// 在范围内,执行操作
.L_out:
你发现没有?CCMP 把两次比较合并成了一次条件比较,减少了一条分支指令。更重要的是,它让 CPU 可以提前预判后续的跳转方向。
#nzcv 参数的含义:
| 位 | 标志 | 说明 |
|---|---|---|
| bit[3] | N | 负数标志 |
| bit[2] | Z | 零标志 |
| bit[1] | C | 进位/借位标志 |
| bit[0] | V | 溢出标志 |
比如 #4 就是 0100,表示 Z=0, C=1, N=0, V=0。这个值的选择取决于你想让后续的条件判断得到什么结果。
避坑指南:我曾经在写一个状态机时,用 CCMP 实现多层条件判断。结果调试了整整一天,发现 #nzcv 参数设错了。记住:#nzcv 的值决定了条件不满足时标志位的状态,这个状态必须能让后续的条件跳转走正确的路径。建议先在纸上画好真值表再写代码。
3.4 三种指令的对比与选择
我把这三种指令的适用场景整理了一下:
| 指令 | 适用场景 | 性能代价 | 推荐指数 |
|---|---|---|---|
| B.cond | 分支内部代码量大(>5条指令)、有函数调用、循环 | 预测成功:1-2周期 预测失败:20-40周期 |
★★☆☆☆ |
| CSEL | 简单的条件赋值、绝对值、min/max | 1-2周期(无分支) | ★★★★★ |
| CCMP | 复杂条件判断(&&、||)、范围检查 | 1-2周期(减少分支) | ★★★★☆ |
我个人习惯是:能用 CSEL 绝不用 B.cond,能用 CCMP 减少分支就尽量用。但也不是绝对的,如果分支内部有几十条指令,那分支预测的收益可能更大。
3.5 知识体系总览
下面这张图把本章的核心逻辑串起来了:
3.6 实战建议
说了这么多,我总结几条实战经验:
- 热点路径优先用 CSEL:在循环内部、频繁执行的路径上,能用 CSEL 替代 B.cond 就替代。性能提升立竿见影。
- CCMP 适合范围检查:判断一个数是否在某个区间内,用 CCMP 可以减少一条分支指令。
- 不要滥用:如果分支内部有函数调用、循环或者大量计算,B.cond 可能更合适。CSEL 和 CCMP 只适合简单的条件操作。
- 用 perf 验证:我每次做完优化,都会用 perf stat 检查分支预测失败率。如果 branch-misses 占比超过 5%,就说明还有优化空间。
调试小技巧:用 GDB 调试条件执行代码时,可以设置断点观察 NZCV 标志位的变化。命令是 info registers cpsr,或者用 p $cpsr。看到标志位的变化,你就知道条件判断是怎么走的了。
嗯,条件执行这块内容就讲到这里。记住一句话:分支是性能的敌人,条件选择是分支的克星。写 ARM64 代码时,多想想能不能用 CSEL 和 CCMP 替代分支,你的代码会跑得更快。