为什么在 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一个bool
或char
,你仍然可以这样做. 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
或函数调用之前的类似内容,因此您可以将此依赖链耦合到其中。
这段代码
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一个bool
或char
,你仍然可以这样做
不幸的是,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 时
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 上有
如果您不覆盖比较输入之一,则将单独的目标寄存器异或零。 setcc %al
/ movzbl %al, %eax
after cmp %esi, %edi
将是所有可能选项中最糟糕的,因为 RAX 可能最后被写入了一个独立的缓存未命中负载,或者一个缓慢的 div
或函数调用之前的类似内容,因此您可以将此依赖链耦合到其中。