movzbl 后接 testl 是否比 testb 快?

Is movzbl followed by testl faster than testb?

考虑这个 C 代码:

int f(void) {
    int ret;
    char carry;

    __asm__(
        "nop # do something that sets eax and CF"
        : "=a"(ret), "=@ccc"(carry)
    );

    return carry ? -ret : ret;
}

当我用 gcc -O3 编译它时,我得到这个:

f:
        nop # do something that sets eax and CF
        setc    %cl
        movl    %eax, %edx
        negl    %edx
        testb   %cl, %cl
        cmovne  %edx, %eax
        ret

如果我将 char carry 更改为 int carry,我会得到这个:

f:
        nop # do something that sets eax and CF
        setc    %cl
        movl    %eax, %edx
        movzbl  %cl, %ecx
        negl    %edx
        testl   %ecx, %ecx
        cmovne  %edx, %eax
        ret

该更改将 testb %cl, %cl 替换为 movzbl %cl, %ecxtestl %ecx, %ecx。不过,该程序实际上是等价的,GCC 知道这一点。作为证据,如果我使用 -Os 而不是 -O3 进行编译,那么 char carryint carry 都会产生完全相同的程序集:

f:
        nop # do something that sets eax and CF
        jnc     .L1
        negl    %eax
.L1:
        ret

看来两件事中的一件必须是真的,但我不确定是哪一件:

  1. A testbmovzbl 后跟 testl 快,因此 GCC 将后者与 int 一起使用是一个错过的优化。
  2. A testbmovzbl 后跟 testl 慢,因此 GCC 将前者与 char 一起使用是错过了优化。

我的直觉告诉我,额外的指令会变慢,但我也有一个挥之不去的疑问,即它是否会阻止我看不到的部分寄存器停顿。

顺便说一句,在我的真实示例中,通常推荐的 xorsetc 之前将寄存器清零的方法不起作用。你不能在内联汇编运行之后这样做,因为 xor 会覆盖进位标志,你不能​​在内联汇编运行之前这样做,因为在 the real context of this code 中,每个通用调用-被破坏的寄存器已经以某种方式被使用了。

据我所知,使用 testmovzb.

读取字节寄存器没有任何缺点

如果您要进行零扩展,那么在 asm 语句之前不对 reg 进行异或零操作也是一个错过的优化,并且 setc 进入其中,因此零扩展的成本不在关键路径。 (在 Intel IvyBridge+ 以外的 CPU 上 movzx r32, r8 不是零延迟)。当然,假设有免费注册。最近的 GCC 有时确实发现了这种 zero/set-flags/setcc 优化,用于从标志设置指令生成 32 位布尔值,但当事情变得复杂时经常会错过它。

对你来说幸运的是,你的实际用例无论如何都无法进行优化(除了 mov [=14=], %eax 归零,这将偏离延迟的关键路径,但会导致 Intel P6 上的部分寄存器停顿family, and cost more code size.) 但它仍然是您的测试用例的优化失误。