为什么在 SETcc 之前进行 XOR?

Cur*_*ous 3 c++ x86 assembly micro-optimization

这段代码

int foo(int a, int b)
{ 
    return (a == b);
}
Run Code Online (Sandbox Code Playgroud)

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

foo(int, int):
        xorl    %eax, %eax
        cmpl    %esi, %edi
        sete    %al
        ret
Run Code Online (Sandbox Code Playgroud)

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

[SETcc] 根据状态标志的设置将目标操作数设置为 0 或 1

那么,如果根据无论如何结果为零/一,那么首先%eax用零初始化有什么意义呢?是不是gcc和clang出于某种原因都无法避免浪费CPU时钟?xorl %eax, %eaxa == b

Pet*_*des 12

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

即使您确实只想返回一个boolor char,您仍然可以这样做以避免在编写 AL 时出现错误的依赖关系。异或归零不花费“一个周期”,它花费 1 uop(并且与 Intel 上的 nop 一样便宜),但这仍然不是免费的。( https://agner.org/optimize/ )

不幸的是 AMD64 没有改变setcc,也没有任何后来的扩展,所以即使使用-march=icelake-client或 ,在 x86 上生成 32 位 0/1 仍然很痛苦znver3。有一个66操作数大小或rep前缀修改setcc为使用32位操作数大小会有所帮助,以避免浪费的指令(和前端UOP)这一点,但无论厂商曾经不屑于引入这样的扩展。(通常只有可以在一些“热”函数中提供主要加速的扩展,您可以对其进行动态调度,而不是需要在任何地方使用以增加小改进的东西。)

setcc 之前的异或归零是最不坏的方法,当您有备用寄存器时,如我在回答的底部讨论的在 x86 程序集中将寄存器设置为零的最佳方法是什么:xor、mov 或 and?.


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

1. mov-imm32=0 可以比较执行,不影响 FLAGS:

# for example if you want to replace a compare input with a boolean
    cmp    %ecx, %eax
    mov    $0, %eax
    setcc  %al
Run Code Online (Sandbox Code Playgroud)

这会浪费代码大小(mov 与 xor 为 5 个字节,而 mov 与 xor 为 2 个字节),并且(在 Intel P6 系列上)在读取 EAX 时会出现部分寄存器停顿,因为没有使用 xor-zeroing 来设置内部 RAX=AL upper -bytes-known-zero 状态。

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

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

2. 8 位 setcc 结果上的 MOVZX

    cmp    %ecx, %eax
    setcc  %cl
    movzbl %cl, %eax
Run Code Online (Sandbox Code Playgroud)

这通常更糟,除了在 P6 系列上它避免了部分寄存器停顿。

movzx处于从准备好比较输入到准备好 0/1 结果的关键路径上。(虽然IvyBridge的和更高版本可以零延迟运行它时,它的两个独立的寄存器之间,这就是为什么我用%cl的不是%al。编译器通常不优化对于这一点,并会setcc %al/movzbl %al, %eax如果他们不设法XOR零东西第一. 即使在拥有它的 Intel CPU 上,这也击败了 mov-elimination。)

setcc %cl一个错误的依赖于RCX(除英特尔P6系列,从全寄存器重命名分别寄存器low8),但没关系,因为RCX和RAX是依赖链导致setcc两个已经部分。

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