为什么 GCC 再次使用 movzbl 对已经零扩展的寄存器进行零扩展?

Itz*_*xic 9 c++ assembly gcc x86-64 cpu-architecture

我想知道为什么这段代码:

size_t hash_word(const char* c, size_t size) {
    size_t hash = uchar(c[0]);
    hash ^= uchar(c[size - 1]);
    hash ^= uchar(c[size - 2]);
    return hash;
}
Run Code Online (Sandbox Code Playgroud)

编译时:

    movzbl  -1(%rcx,%rdx), %eax
    xorb    -2(%rcx,%rdx), %al
    xorb    (%rcx), %al
    movzbl  %al, %eax            <-
    ret
Run Code Online (Sandbox Code Playgroud)

结果是第二条 movzbl 行。

我转换为asmg++ -Wall -O3 -S file.cpp

按照我的理解,%eax 的所有高位应该已经从第一个 movzbl 开始设置为零。那么下面的两个 xorb 不应该修改任何高位,因为它只触及 %al 中的位。那么为什么还需要额外的说明呢?不是应该在前三个之后再做吗?

Pet*_*des 7

当调用者在写入 AL 之后读取 RAX 时,P6 系列 CPU 会出现部分寄存器停顿xorb

但在 Sandybridge 上,低字节寄存器上的 RMW 是完整寄存器上的 RMW,而不是像那样将其与完整寄存器分开重命名mov mem, %al。在 Ivy Bridge 或 Haswell 上,与完整寄存器分开的低字节寄存器的重命名被完全删除(仅保留高字节重命名,如 AH/BH/CH/DH,因为这仍然足以使 AL 和 AH 独立。) Evenmov mem, %al是一个负载 + 微融合 ALU uop,用于合并现代 Intel P 核上的低字节以及其他所有内容。部分寄存器重命名在任何其他微架构系列上都不是问题,例如 P4、Silvermont 或任何 AMD。


默认值-mtune=generic不应该太关心 Nehalem 和更早的版本。而且 GCC/Clang 每晚构建仍然使用movzbl(又名 Intel movzx)和-mtune=skylake-march=skylake、 或-mtune=znver1( Godbolt ),所以大概这个代码生成选择是内置的,而不是更新tune=generic设置的问题。

也许历史上是因为P6 的工作方式,而且现在 CPU 不同了,没有人愿意去改变它。这种事并不少见。或者可能是因为他们不知道如何在不犯错误的情况下进行此优化,例如在其他情况下仅写入寄存器的低 8 位来创建错误的依赖关系。

GCC 和 clang 确实知道这hash < 0x100是一个常量1,因此如果他们想查找的话,他们确实有足够的值范围跟踪来知道该值已经是零扩展字节。


您可以使用 Missed-optimization 关键字在GCC 的 bugzilla上报告此问题,也可以在 Clang 的问题跟踪器上报告此问题,https://github.com/llvm/llvm-project/issues

在你的最小示例中使用unsigned char,这样你就不必定义uchar,就像这个答案中的 Godbolt 链接一样。请随意引用我在编译器错误报告中写的任何内容,并链接此问答。


不是应该在前三个之后再做吗?

你的意思是第一次加载movb -1(%rcx,%rdx), %al?由于对 RAX 的错误依赖,情况会更糟。MOVZX 通常是加载 byte 的最有效方法,除非您正在优化代码大小。在这种情况下,我们稍后需要零扩展,因此到目前为止,最好的选择是简单地删除movzbl %al, %eax并保持其他所有内容相同。也许这就是您的意思,该函数应该在前三个之后完成,准备返回。

RISC ISA具有零扩展或符号扩展加载,而不具有将字节合并到旧值的加载,因为它使目标变为只写。 mov mem, %al这是只有 x86 才有的奇怪的事情。


顺便说一句,如果您确实需要在某些操作后进行零扩展,那么在同一个寄存器中是最糟糕的选择,但这就是 GCC 和 Clang 所做的。自 Haswell 或 IvB 以来,他们似乎没有针对movzx英特尔的零延迟进行优化。(也许是最近的AMD,我忘了?) x86的MOV真的可以“免费”吗?为什么我根本无法重现这个?

如果您正在针对现代英特尔和 P6 系列的混合进行调整(通过movzx在末尾添加 ),您可以通过以下方式减少对现代英特尔的影响:

    movzbl  -1(%rcx,%rdx), %r8d
    xorb    -2(%rcx,%rdx), %r8b
    xorb    (%rcx), %r8b
    movzbl  %r8b, %eax   # avoid same-register if we keep this for P6's benefit
    ret
Run Code Online (Sandbox Code Playgroud)

这会为 REX 前缀带来额外的代码大小,因为 Windows x64 调用约定不会留下任何被调用破坏的寄存器,这些寄存器的低字节部分没有 REX 前缀(RAX 除外)。

如果纯粹针对 P6 系列进行优化,是的,您可以从加载movbAL.

  • @ecm:P6 **系列**微架构包括 P6 到 Nehalem。Core 2 和 Nehalem 是支持 x86-64 的。(Sandybridge 通常被认为是一个足够重大的变化,足以成为一个新系列,例如具有 PRF 和 uop 缓存。https://www.realworldtech.com/sandy-bridge/)。此外,如果这是可以在 P6 系列的所有成员和现代 CPU 上运行的 32 位代码,同样的要点也适用。 (2认同)