为什么 clang 和 GCC 不使用 xchg 来实现 std::swap?

Jan*_*tke 1 c++ x86 swap compilation

我有以下代码:

char swap(char reg, char* mem) {
    std::swap(reg, *mem);
    return reg;
}
Run Code Online (Sandbox Code Playgroud)

我希望这可以编译为:

swap(char, char*):
    xchg    dil, byte ptr [rsi]
    mov     al, dil
    ret
Run Code Online (Sandbox Code Playgroud)

但它实际编译成的是 (at -O3 -march=haswell -std=c++20):

swap(char, char*):
    mov     al, byte ptr [rsi]
    mov     byte ptr [rsi], dil
    ret
Run Code Online (Sandbox Code Playgroud)

这里进行了现场演示

从 的文档中xchg,第一种形式应该是完全可能的:

XCHG - 用寄存器交换寄存器/内存

交换目标(第一个)和源(第二个)操作数的内容。操作数可以是两个通用寄存器或一个寄存器和一个内存位置。

那么编译器不能xchg在这里使用有什么特别的原因吗?我也尝试过其他示例,例如交换指针、交换三个操作数、交换类型以外的其他类型,char但我从未xchg在编译输出中得到 an 。怎么来的?

Pet*_*des 8

TL:DR:因为编译器针对速度进行优化,而不是针对听起来相似的名称。还有很多其他可怕的方式他们也可以实施它,但选择不这样做。

带有 mem 的 xchg 有一个隐式lock前缀(在 386 及更高版本上),所以它非常慢。你总是想避免它,除非你需要一个原子交换,或者对代码大小完全没有优化关爱所有的性能,在你想要的结果,同一个寄存器的原始值的情况。有时在天真(性能无视)或代码高尔夫手写冒泡排序中看到,作为交换 2 个内存位置的一部分。

可能clang -Oz会变得疯狂,IDK,但希望在这种情况下不会,因为您的 xchg 方式是更大的代码大小,需要在两条指令上使用 REX 前缀来访问 DIL,而 2-mov 方式是一个 2 字节和一个3 字节指令。 clang -Oz确实会做push 1/pop rax之类的事情,而不是mov eax, 1节省 2 字节的代码大小。

GCC-Os将不使用xchg的不需要是原子的,因为掉期-Os仍在乎一些关于速度。


另外,IDK 为什么你会认为 xchg + 依赖 mov 会比两个mov可以并行运行的独立指令更快或更好的选择。(存储缓冲区确保存储在加载后正确排序,无论哪个 uop 首先发现其执行端口空闲)。

https://agner.org/optimize/和其他环节https://stackoverflow.com/tags/x86/info

说真的,我只是看不出有什么合理的理由让您认为编译器可能想要使用xchg,尤其是考虑到调用约定没有在 RAX 中传递参数,因此您仍然需要 2 条指令。即使对于寄存器,xchg reg,reg在 Intel CPU 上也是 3 uops,它们是无法从 mov-elimination 中受益的微码 uops。(有些 AMD CPU 有 2-uop xchg reg,reg为什么 XCHG reg, reg 是现代 Intel 架构上的 3 micro-op 指令?


我也猜你在看 clang 输出;即使返回值只是低字节,GCC 也会通过使用movzx eax, byte ptr [rsi]加载来避免部分寄存器恶作剧(如错误依赖)。零扩展负载比合并到 RAX 的旧值更便宜。所以这是xchg.