3.10 条件分支

条件分支是编程语言中 if-else 语句的底层实现。x86-64 提供了两种实现条件分支的方式:条件跳转条件传送

if-else 的汇编实现

基本模式

C 语言的 if-else 语句在汇编中通过 cmp + 条件跳转实现:

// C 源代码
long absdiff(long a, long b) {
    long result;
    $if (a \ge  b) {$
        result = a - b;
    } else {
        result = b - a;
    }
    return result;
}
# 汇编实现(条件跳转版本)
absdiff:
    cmpq    %rsi, %rdi          # 比较 a (%rdi) 和 b (%rsi)
    jl      .Lelse              # 如果 a < b,跳转到 else 分支
    $subq    %rsi, %rdi          # a \ge  b: result = a - b$
    movq    %rdi, %rax
    jmp     .Ldone
 
.Lelse:
    subq    %rdi, %rsi          # a < b: result = b - a
    movq    %rsi, %rax
 
.Ldone:
    ret

控制流图

        ┌──────────┐
        │  cmpq a,b │
        │  jl .Lelse │
        └────┬─────┘
             │
      ┌──────┴──────┐
      ▼             ▼
  ┌──────┐     ┌──────┐
  │ then │     │ else │
  │a-b   │     │b-a   │
  └──┬───┘     └──┬───┘
      └──────┬────┘
             ▼
         ┌──────┐
         │ done │
         │ ret  │
         └──────┘

条件传送指令 cmov

条件传送(conditional move)是一种无分支的条件操作实现,通过数据依赖替代控制依赖。

原理

cmov 指令根据 的状态有条件地执行数据传送

cmovXX   源操作数, 目的操作数
# 如果条件 XX 为真,则执行:目的操作数 = 源操作数
# 如果条件 XX 为假,则什么也不做(空操作)

cmov 指令族

指令条件条件码
cmove (或 cmovz)相等ZF
cmovne (或 cmovnz)不等~ZF
cmovs负数SF
cmovns非负~SF
cmovl小于 (有符号$)SF ≠ OF
cmovge大于等于 (有符号)SF = OF
cmovle小于等于 (有符号$)(SF≠OF) | ZF
cmovg大于 (有符号)(SF=OF) & ~ZF
cmovb低于 (无符号)CF
cmovae高于等于 (无符号)~CF

使用 cmov 重写 absdiff

# 条件传送版本(无分支)
absdiff_cmov:
    subq    %rsi, %rdi          #计算 $a - b → %rdi$
    subq    %rdi, %rsi          #计算 $b - a → %rsi$
    # 现在 %rdi = a-b, %rsi = b-a
    cmpq    $0, %rdi            # 检查 a-b 的符号 (即 a >= b ?)
    cmovl   %rsi, %rdi          # 如果 a-b < 0 (即 a < b),选择 b-a
    movq    %rdi, %rax          # 返回选中的值
    ret

条件传送的优缺点

优点

  • 完全无分支,避免分支预测失败惩罚
  • 执行时间与条件无关,适合不可预测的分支

缺点

  • 两个分支都会被求值(计算两个结果),增加计算量
  • 如果分支包含函数调用、指针解引用或有副作用,不能使用条件传送
// 不适合条件传送的情况(有副作用)
if (condition) {
    *ptr = compute();  // 指针解引用
} else {
    *ptr = 0;
}

性能对比

适用场景条件跳转条件传送
分支高度可预测(如循环边界检查)优秀一般
分支不可预测(如随机数据排序)较差优秀
分支计算量很大较好(只执行一个分支)较差(计算两个分支)
分支计算量很小一般较好

编译器对分支的优化

条件跳转 条件传送

// 原始 C 代码
long max(long a, long b) {
    return (a > b) ? a : b;
}
# gcc -O1 条件跳转版本
max_jump:
    cmpq    %rsi, %rdi
    jle     .Luse_b
    movq    %rdi, %rax
    ret
.Luse_b:
    movq    %rsi, %rax
    ret
# gcc -O2 条件传送版本(优化后)
max_cmov:
    cmpq    %rsi, %rdi          # 比较 a, b
    movq    %rsi, %rax          # rax = b (预先加载 b)
    cmovg   %rdi, %rax          # 如果 a > b, rax = a
    ret

选择合适优化策略

编译器基于分支预测代价模型决定使用哪种方式:

if (预测难度 × 分支代价 > 两个分支的总代价) {
    使用 cmov
} else {
    使用 条件跳转
}

set 指令:条件码设置

set 系列指令根据条件码将单字节寄存器设置为 0 或 1。常用于实现布尔表达式。

// C 代码
int less_than(long a, long b) {
    return a < b;
}
less_than:
    cmpq    %rsi, %rdi          # 比较 a (%rdi) 和 b (%rsi)
    setl    %al                 # al = (a < b) ? 1 : 0
    movzbq  %al, %rax           # 零扩展到 64 位
    ret

常用 set 指令:

指令含义示例
sete DD = (==)sete %al
setne DD = (!=)setne %al
setl DD = (<, 有符号)setl %al
setg DD = (>, 有符号)setg %al
setb DD = (<, 无符号)setb %al
seta DD = (>, 无符号)seta %al

相关笔记

    • 条件判断的基础
    • 条件跳转指令详解
    • 循环中的条件分支

链接到