【问题标题】:Is movzbl followed by testl faster than testb?movzbl 后跟 test 比 test 快吗?
【发布时间】:2020-10-10 02:53:54
【问题描述】:

考虑这个 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, %ecx 和testl %ecx, %ecx。不过,该程序实际上是等价的,而且 GCC 知道这一点。作为证据,如果我使用-Os 而不是-O3 进行编译,那么char carry 和int carry 都会产生完全相同的程序集:

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

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

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

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

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

【问题讨论】:

  • 还有一个基于 CF 的条件否定的替代序列(比这个短),你认为这个主题吗?
  • @harold This one?
  • 这也不错,但我想到的是sbb / xor / sub
  • @harold 弄清楚哪些操作数去哪里进行这项工作是一项很好的大脑练习(看起来像sbb %ecx, %ecx;xor %ecx, %eax;sub %ecx, %eax)。无论如何,是的,我肯定在寻找那些聪明的想法。我想知道处理器是否足够聪明,可以避免对ecx 的错误依赖。
  • AMD 的处理器是(根据 Agner Fog 的说法),据我所知,英特尔仍在读取 ecx

标签: performance assembly x86 x86-64 micro-optimization


【解决方案1】:

我知道使用test 与movzb 读取字节寄存器没有任何缺点。

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

幸运的是,您的实际用例无论如何都无法进行该优化(mov $0, %eax 归零除外,这将偏离延迟的关键路径,但会导致 Intel P6 系列上的部分寄存器停止,并且成本更多的代码大小。)但它仍然是您的测试用例错过的优化。

【讨论】:

  • 现在我想知道leal -1(%eax), %edx; notl %edx; cmovcl %edx, %eax 会比保存和重新测试标志更快。 (而且我也开始怀疑“编译器比你更擅长优化;让它完成它的工作”。)
  • @JosephSible-ReinstateMonica:是的,当然值得考虑。是的,编译器非常擅长大规模,在几秒钟内进行持续传播和内联以创建手工无法维护和/或需要数年时间编写的 asm。但是在像单个循环或函数这样的小范围内,编译器通常不是最优的。该建议大多数时候适用于大多数人,但通常不适用于阅读并理解 Agner Fog 的 microarch PDF (agner.org/optimize) 的人。当然,实际使用 asm 编写的不利之处在于,未来 CPU 上的优势可能会有所不同
  • @JosephSible-ReinstateMonica:请记住,编译速度仍然是一个因素,因此编译器通常需要避免使用 O(e^N) 或更差时间复杂度的算法,除非 N 很小且固定(例如机器中的寄存器数量)。因此,如果您想要当前 CPU 的最佳代码,您需要让编译器做得更好(C++ code for testing the Collatz conjecture faster than hand-written assembly - why?),或者只是手动编写 asm。特别是对于像扩展精度这样的东西,你想要一个 adc 链。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-12-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-06
  • 2022-11-06
相关资源
最近更新 更多