为什么要使用第二个 movzbl 进行编译?

问题描述 投票:0回答:1

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

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;
}

编译时:

    movzbl  -1(%rcx,%rdx), %eax
    xorb    -2(%rcx,%rdx), %al
    xorb    (%rcx), %al
    movzbl  %al, %eax            <-
    ret

产生第二条 movzbl 行。

我用“g++ -Wall -O3 -S file.cpp”转换为asm

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

c++ assembly gcc x86-64 cpu-architecture
1个回答
0
投票

当调用者在

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

但是在 Sandybridge 上,低字节寄存器上的 RMW 是完整寄存器上的 RMW,而不是像

mov mem, %al
那样将其与完整寄存器分开重命名。在 Ivy Bridge 或 Haswell 上,与完整寄存器分开的低字节寄存器的重命名被完全删除(仅保留高字节重命名,如 AH/BH/CH/DH,因为这仍然足以独立使用 AL 和 AH。 )因此,即使
mov 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 链接一样。请随意引用我在编译器错误报告中写的任何内容,并链接此问答。

© www.soinside.com 2019 - 2024. All rights reserved.