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

Why are clang and GCC not using xchg to implement std::swap?

我有以下代码:

char swap(char reg, char* mem) {
    std::swap(reg, *mem);
    return reg;
}

我预计这会编译为:

swap(char, char*):
    xchg    dil, byte ptr [rsi]
    mov     al, dil
    ret

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

swap(char, char*):
    mov     al, byte ptr [rsi]
    mov     byte ptr [rsi], dil
    ret

参见 here for a live demo

根据xchg的文档,第一种形式应该是完全可行的:

XCHG - Exchange Register/Memory with Register

Exchanges the contents of the destination (first) and source (second) operands. The operands can be two general-purpose registers or a register and a memory location.

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

So is there any particular reason why it's not possible for the compiler to use xchg here?

因为 movxchg 快并且编译器针对速度进行了优化。

参见:

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

xchg with mem 有一个隐含的 lock 前缀(在 386 和更高版本上)所以它非常慢 。你总是想避免它,除非你 需要 原子交换,或者正在完全优化 code-size 而根本不关心 性能,在您确实希望结果与原始值位于同一寄存器中的情况下。有时在天真(性能无视)或 code-golfed hand-written 冒泡排序中看到作为交换 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 仍然关心 some速度。


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

https://agner.org/optimize/ and other links in https://whosebug.com/tags/x86/info

说真的,我只是看不出有什么合理的理由让你认为编译器可能想要使用 xchg,特别是考虑到调用约定没有在 RAX 中传递 arg,所以你仍然需要 2 条指令。即使对于寄存器,Intel CPU 上的 xchg reg,reg 也是 3 微码,它们是无法从 mov-elimination 中受益的微码微码。 (一些 AMD CPU 有 2-uop xchg reg,reg


我还猜想您正在查看 clang 输出; 通过使用 movzx eax, byte ptr [rsi] 加载,即使 return 值只是低字节。 Zero-extending 负载比合并到 RAX 的旧值便宜。所以这是 xchg.

的另一个缺点