为什么在 SETcc 之前异或?

Why XOR before SETcc?

这段代码

int foo(int a, int b)
{ 
    return (a == b);
}

生成以下程序集 (https://godbolt.org/z/fWsM1zo6q)

foo(int, int):
        xorl    %eax, %eax
        cmpl    %esi, %edi
        sete    %al
        ret

根据https://www.felixcloutier.com/x86/setcc

[SETcc] Sets the destination operand to 0 or 1 depending on the settings of the status flags

那么,如果 %eax 取决于 a == b 的结果,首先执行 xorl %eax, %eax%eax 初始化为零有什么意义呢? gcc 和 clang 出于某种原因都无法避免 CPU 时钟的浪费吗?

因为 setcc 很烂:仅适用于 8 位操作数大小。但是您使用 32 位 int 作为 return 值,因此您需要将 8 位结果零扩展为 32 位。

即使你做了只想return一个boolchar,你仍然可以这样做. xor-zeroing doesn't cost "a cycle", it costs 1 uop (and is as cheap as a nop on Intel), but that's still not free. (https://agner.org/optimize/)

不幸的是,AMD64 没有改变 setcc,也没有任何后来的扩展,因此即使使用 -march=icelake-client 或 [=17=,在 x86 上生成 32 位 0/1 仍然很痛苦].使用 66 操作数大小或 rep 前缀修改 setcc 以使用 32 位操作数大小将有助于避免为此浪费指令(和前端 uop),但是两家供应商都没有费心引入这样的扩展。 (通常只有可以在一些“热门”功能中提供主要加速的扩展,您可以对其进行动态调度,而不是需要在任何地方使用的东西加起来才能实现小的改进。)

setcc 之前的异或归零是最不坏的方法,当你有备用寄存器时,正如我在 上的回答底部所讨论的那样。


如果您想覆盖比较输入,其他选项包括:

1. mov-imm32=0 你可以在 之后 比较,不影响 FLAGS:

# for example if you want to replace a compare input with a boolean
    cmp    %ecx, %eax
    mov    [=10=], %eax
    setcc  %al

这会浪费代码大小(对于 mov 与 xor 分别为 5 个字节和 2 个字节),并且在读取 EAX 时 ,因为没有使用异或归零来设置内部 RAX=AL 高字节-已知零状态。

mov-immediate 不在关键路径上,因此无序执行可以在比较输入准备好之前尽早完成它,并准备好清零寄存器以供 setcc 写入。

(在 Intel SnB 系列 CPU 上,异或归零在重命名逻辑中处理,因此不必提前执行以准备好零;它在进入后端时已经完成。例如在前端停顿之后,xor-zeroing 和 setcc 可以在同一个周期中进入后端,但是 setcc 仍然可以在那之后的第一个周期中执行,不像它是一个 mov-immediate 实际上必须 运行 在后端执行单元上将零写入寄存器。)

2。 8 位 setcc 结果上的 MOVZX

    cmp    %ecx, %eax
    setcc  %cl
    movzbl %cl, %eax

这在大多数情况下甚至更糟,除了在 P6 系列上它避免了部分寄存器停顿。

但是 movzx 处于从准备好比较输入到准备好 0/1 结果的关键路径上。 (尽管 ,这就是为什么我使用 %cl 而不是 %al 的原因。编译器通常不会为此进行优化,如果它们会 setcc %al / movzbl %al, %eax不要首先对某些东西进行异或归零。即使在具有它的 Intel CPU 上,这也会击败移动消除。)

setcc %cl 在 RCX 上有 (Intel P6 系列除外,它重命名 low8 寄存器与完整寄存器分开),但这没关系,因为 RCX 和 RAX 都已经是依赖项的一部分通往 setcc 的链。

如果您不覆盖比较输入之一,则将单独的目标寄存器异或零。 setcc %al / movzbl %al, %eax after cmp %esi, %edi 将是所有可能选项中最糟糕的,因为 RAX 可能最后被写入了一个独立的缓存未命中负载,或者一个缓慢的 div 或函数调用之前的类似内容,因此您可以将此依赖链耦合到其中。