条件执行指令:CSEL / CSINC / CSINV / CSNEG
条件执行,说白了就是让指令自己决定「做不做」。不用跳转,不用打乱流水线。我刚开始接触 ARM64 时,总觉得条件执行是锦上添花,后来在视频编解码项目里被分支预测惩罚搞怕了,才真正体会到它的价值。
CSEL:条件选择
CSEL 是条件执行里最基础的指令。它的语义很简单:如果条件成立,选第一个操作数;否则选第二个。
// 伪代码:if (cond) Rd = Rn; else Rd = Rm;
CSEL X0, X1, X2, EQ // 如果 Z=1,X0 = X1;否则 X0 = X2
我在做网络协议栈时,经常用 CSEL 替代短小的 if-else。比如根据包类型选择处理函数指针:
// 根据 w0 是否为 0,选择不同处理函数地址
CSEL x3, x1, x2, NE // w0 != 0 时选 x1,否则选 x2
BR x3 // 间接跳转
CSINC:条件选择并递增
CSINC 在 CSEL 基础上多做了一个加法:条件成立时选 Rn,不成立时选 Rm+1。这个指令在边界检查、循环计数场景里特别好用。
// 伪代码:if (cond) Rd = Rn; else Rd = Rm + 1
CSINC X0, X1, X2, EQ
我记得有一次优化字符串处理函数,需要根据比较结果决定是否递增指针。用 CSINC 一条指令搞定,比先 CSEL 再 ADD 省了一条指令,也省了一个临时寄存器。
| 场景 | 传统写法 | CSINC 写法 | 指令节省 |
|---|---|---|---|
| 边界检查+递增 | CMP + B.cond + ADD | CMP + CSINC | 1 条指令 + 1 个标签 |
| 最大值计算 | CMP + CSEL + ADD | CMP + CSINC | 1 条指令 |
CSINV:条件选择并取反
CSINV 在条件不成立时对 Rm 按位取反。这个指令在实现「不等于」逻辑、掩码翻转时非常顺手。
// 伪代码:if (cond) Rd = Rn; else Rd = ~Rm
CSINV X0, X1, X2, GT // 如果大于,选 X1;否则选 ~X2
嗯,这里要注意:取反是按位取反,不是算术取负。想要算术取负,得用 CSNEG。
~Rm,不是 -Rm。有一次我把 CSINV 当 CSNEG 用,结果算出来的符号位全反了,调试了半小时才发现。
CSNEG:条件选择并取负
CSNEG 在条件不成立时对 Rm 做算术取负(即 0 - Rm)。这个指令在绝对值计算、符号调整里是神器。
// 伪代码:if (cond) Rd = Rn; else Rd = -Rm
CSNEG X0, X1, X2, LT // 如果小于,选 X1;否则选 -X2
绝对值函数用 CSNEG 实现,干净利落:
// int64_t abs(int64_t x) 的 ARM64 实现
// x 在 X0,结果放 X0
CMP X0, #0
CSNEG X0, X0, X0, GE // x >= 0 时选 x,否则选 -x
条件执行 vs 分支指令:性能对比
为什么我要花这么多篇幅讲条件执行?因为分支指令有代价。现代处理器有深度流水线,分支预测错了就要冲刷流水线,损失 10-20 个周期。
条件执行指令没有分支,没有预测,没有惩罚。代价是什么?条件执行指令的两条路径都要计算,只是最后选一个写回。如果两条路径都很短(1-2 条指令),条件执行完胜。如果某条路径很长,条件执行反而浪费。
| 对比维度 | 条件执行 (CSEL 等) | 分支指令 (B.cond) |
|---|---|---|
| 流水线影响 | 无分支,流水线平滑 | 预测错误时冲刷 10-20 级 |
| 代码密度 | 高,无标签跳转 | 低,需要标签和跳转指令 |
| 适用分支体大小 | ≤ 3 条指令 | 任意大小 |
| 寄存器压力 | 两条路径都需要准备操作数 | 只有选中路径需要准备 |
| 可预测性 | 完全确定,无预测 | 依赖分支预测器 |
你想想看,一个分支预测错误损失 15 个周期,而条件执行最多多算 2-3 条指令。在热点路径上,这个差距就是几倍的性能差异。
最后说一句:条件执行指令不是银弹。它要求两条路径的操作数都提前准备好,这会增加寄存器压力。我在做数据库引擎优化时,就遇到过寄存器不够用,不得不把条件执行改回分支的情况。工具好不好,关键看你怎么用。