【问题标题】:x86 ASM: useless conditional jump?x86 ASM:无用的条件跳转?
【发布时间】:2021-04-18 15:21:42
【问题描述】:

我正在查看以下 x86 汇编代码(Intel 语法):

movzx   eax, al
and     eax, 3
cmp     eax, 3
ja      loc_6BE9A0

在我的理解中,这应该等同于 C 语言中的内容:

eax &= 0xFF;
eax &= 3;
if (eax > 3)
   loc_6BE9A0();

这似乎没有多大意义,因为这个条件永远不会为真(因为如果eax 之前与 3 进行了和-ed,则它永远不会大于 3)。我在这里遗漏了什么还是这真的只是一个不必要的条件?

另外:如果movzx eax, al 在那之后立即与 3 相加,那么它也不应该是必需的,是吗?

我问这个是因为我对汇编语言不太熟悉,所以我不完全确定我是否在这里遗漏了什么。

【问题讨论】:

  • 是的,你是对的。
  • 你从哪里得到的 asm / 机器码?来自 C 编译器的调试版本?
  • 不,一个游戏的发布版本:D

标签: assembly x86 intel


【解决方案1】:

你是对的:movzx 是多余的,因为下面的 and。它可能是由非优化编译器生成的。

是的,如果这段代码直接执行,那么ja 跳转将永远不会被执行。但是,如果其他地方的代码直接跳转到cmp(甚至跳转到ja),cmp/ja 可能并非完全没用。

【讨论】:

  • 可能 movzx 是为了避免英特尔 P6 系列上的部分寄存器停顿,如果早期的代码只写了 AL 并且稍后实际上需要读取完整的寄存器(所以这段代码不能只使用and al, 3)。但考虑到代码生成的其余部分,它可能只是一个愚蠢的编译器,可能是调试模式下的 MSVC。 (GCC -O0 不会在语句之间进行优化,但它通常不会在单个语句中如此愚蠢。clang 也不是。我假设单个语句,否则 var 会在调试构建中溢出/重新加载到堆栈。​​)
  • 我错了,gcc -O0 al 来自函数返回值 unsigned char 时发出该序列,但在取消引用 unsigned char* 时不会发出。
【解决方案2】:

这是多余的,不是你在优化的 asm 中看到的。

即使cmp/ja 是其他地方的可能跳转目标,现有的优化编译器(如 GCC、clang、MSVC 和 ICC)也会(我很确定)执行jmp 或不同的代码布局,而不是让执行落入一个永远为假的条件分支。优化器会知道在这条执行路径上不需要有条件分支,因此会确保它没有遇到条件分支。 (即使这需要额外花费jmp。)

这可能是一个不错的选择,即使在通过这种方式可以节省一些代码大小的假设情况下,因为您不想用不必要的条件分支污染/稀释分支预测历史,并且分支可能会错误预测为采取了。


但在调试模式下,一些编译器比其他编译器更能够在单个语句或表达式中进行优化。 (Across statements they'd always spill/reload vars to memory,除非你使用了register int foo;

我能够欺骗 clang -O0 和 MSVC 发出确切的指令序列。还有类似的东西,但来自 GCC 的情况更糟。 (令人惊讶的是,gcc -O0 仍然在单个表达式中进行了一些优化,例如对 x /= 10; 使用乘法逆,并为 if(false) 删除死代码。与 MSVC 实际上将 0 放入寄存器并测试它是否为 0。)

void dummy();
unsigned char set_al();
int foo(void) {
    if ((set_al() & 3) <= 3U)
        dummy();
    return 0;
}

clang12.0 for x86-64 Linux (on Godbolt)

        push    rbp
        mov     rbp, rsp
        call    set_al()
        movzx   eax, al            # The redundant sequence
        and     eax, 3
        cmp     eax, 3
        ja      .LBB0_2
        call    dummy()
.LBB0_2:
        xor     eax, eax
        pop     rbp
        ret

MSVC 包含相同的序列。 GCC10.3 类似但更糟糕的是,它在寄存器中实现了一个布尔值并testing 它。 (两者也在同一个 Godbolt 链接中)

## GCC10.3
 ... set up RBP as a frame pointer
        movzx   eax, al            # The redundant sequence
        and     eax, 3
        cmp     eax, 3
        setbe   al
        test    al, al             # even worse than just jnbe
        je      .L2
        call    dummy()
.L2:
        mov     eax, 0
        pop     rbp
        ret

char 来自内存而不是返回值,即使在调试模式下,GCC 也会优化比较:

int bar(unsigned char *p) {
    if ((*p & 3) <= 3U)
        dummy();
    return 0;
}
# GCC 10.3 -O0
bar(unsigned char*):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16                # space to spill the function arg
        mov     QWORD PTR [rbp-8], rdi
        call    dummy()                # unconditional call
        mov     eax, 0
        leave
        ret

clang 和 MSVC 进行测试,都使用 asm 之类的

#MSVC19.28 (VS16.9)  default options (debug mode)
     ...
        movzx   eax, BYTE PTR [rax]
        and     eax, 3
        cmp     eax, 3
        ja      SHORT $LN2@bar
     ...

【讨论】:

    猜你喜欢
    • 2016-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-11
    • 1970-01-01
    • 2017-06-03
    • 1970-01-01
    相关资源
    最近更新 更多