为什么 gcc 和 clang 生成 mov reg,-1

Why do gcc and clang generate mov reg,-1

我正在使用编译器资源管理器查看 gcc 和 clang 的一些输出,以了解这些编译器为某些代码发出的汇编。最近我看了这段代码的输出。

int compare_int64(int64_t left, int64_t right)
{
    return (left < right) ? -1 : (left > right) ? 1 : 0;
}

本练习的重点不是针对 C++,在 C++ 中无论如何都可能内联此代码,而是在调用此类函数时。

对于 -O3,这是输出:

叮当声:

xor     ecx, ecx
cmp     rdi, rsi
setg    cl
mov     eax, -1
cmovge  eax, ecx
ret

gcc:

xor     eax, eax
cmp     rdi, rsi
mov     edx, -1
setg    al
cmovl   eax, edx
ret

我注意到这段代码的大小为 17 字节,比 16 字节多了 1 个字节(我使用的另一个非 C++ 编译器中 x64 的默认代码对齐是 16)。对于显示的 gcc 代码,我正在考虑使用 lea edx,[eax-1]or edx,-1(后者当然在 cmp 之前)来减少代码大小。有趣的是,当使用 -Os 时,gcc 会插入一条 jl 指令,这对于该函数的性能来说是灾难性的。

我不是专家,查看了 Agner Fog 的指令表手册,如果我没有误认为 movleaor,那么 timings/latency 是等于。

所以实际问题: 为什么两个编译器都使用 5 字节大小的指令而不是较短的 3 或 4 字节指令? 将 mov reg,-1 替换为 lea reg,[eax-1]or reg,-1 是否无害?

在优化速度时使用 mov reg, -1 而不是 or reg, -1 因为前者使用寄存器作为“只写”操作数,CPU 知道并使用它来有效地安排它(无序)。尽管 or reg, -1 总是会产生 -1,但 CPU 不将其识别为依赖性破坏(只写)指令。

说明它如何影响性能:

mov eax, [rdi]  # Imagine a cache-miss here.
mov [rsi], eax
mov eax, -1     # `mov eax, -1` is able to dispatch and execute without waiting
                # for the cache-miss to be served.
add eax, edx    # `add eax, edx` only needs to wait 1 cycle for `mov` to
                # complete (assuming `edx` is ready) and then it can
                # dispatch while cache-miss load from a few lines above
                # is still in progress.

现在这个代码:

mov eax, [rdi]   # Imagine a cache-miss here.
mov [rsi], eax
or eax, -1       # Now this instruction has to wait for the cache-miss
                 # load to complete.
add eax, edx     # And this one will be waiting too.

(示例适用于任何当前的 x86-64 CPU,例如 Skylake/Ice Lake/Zen)。

如果您在汇编中编写代码并且确定寄存器不是当前正在进行的依赖链的一部分,您可以使用 or reg, -1 并且它不会产生负面影响(如果当然,你的假设是正确的)。

由于存在意外附加到依赖链的危险,编译器在优化速度时通常不会使用 or reg, -1 来生成 -1。

当我们需要零而不是 -1 时,我们很幸运,因为有 CPU 可以识别的成语,例如 xor reg, regsub reg, reg。它们的代码量较小,并且 CPUs 认识到计算结果不依赖于寄存器(始终为零)。

这些零习语,除了代码量较小之外,通常也是由CPU的前端部分处理,所以根据结果的指令可以立即派发.

零习语也适用于向量寄存器:vpxor xmm0, xmm0, xmm0(产生零而不依赖于 xmm0 的先前值和零延迟)。有意思的是,向量寄存器还有一个-1的惯用语,就是vpcmpeqd xmm0, xmm0, xmm0——这个被识别为只写的(和自己比较的值永远为真),但是它还是要执行(所以它有延迟=1),至少在 SKL/Zen CPUs.

关于生成零的更多信息:

具体识别哪些成语可以在Agner Fog的手册或CPU优化指南中找到。 TLDR 是通用寄存器只有零习语,向量寄存器有零习语和全一习语。

另请参阅:(提及 lea edx, [rax - 1])。


注意实际功能。正如您从汇编中看到的那样,大部分工作实际上是在尝试生成您所请求的特定常量。

如果你打算对 -1,0,1 做的只是判断它是否是 negative/zero/positive,那么最好生成 left - right 只要您确保没有溢出 ,因为这会使减法结果本身不足以进行比较 - 在​​这种情况下,只需使用 -1、0、1),然后仅使用 branch/cmov。