为什么GCC会插入mfence,而Clang不使用它?

7
为什么GCC和Clang针对这段代码(x86_64,-O3 -std=c++17)会产生如此不同的汇编代码?
#include <atomic>

int global_var = 0;

int foo_seq_cst(int a)
{
    std::atomic<int> ia;
    ia.store(global_var + a, std::memory_order_seq_cst);
    return ia.load(std::memory_order_seq_cst);
}

int foo_relaxed(int a)
{
    std::atomic<int> ia;
    ia.store(global_var + a, std::memory_order_relaxed);
    return ia.load(std::memory_order_relaxed);
}

GCC 9.1:

foo_seq_cst(int):
        add     edi, DWORD PTR global_var[rip]
        mov     DWORD PTR [rsp-4], edi
        mfence
        mov     eax, DWORD PTR [rsp-4]
        ret
foo_relaxed(int):
        add     edi, DWORD PTR global_var[rip]
        mov     DWORD PTR [rsp-4], edi
        mov     eax, DWORD PTR [rsp-4]
        ret

Clang 8.0:

foo_seq_cst(int):                       # @foo_seq_cst(int)
        mov     eax, edi
        add     eax, dword ptr [rip + global_var]
        ret
foo_relaxed(int):                       # @foo_relaxed(int)
        mov     eax, edi
        add     eax, dword ptr [rip + global_var]
        ret

我怀疑这里的mfence是过度设计,我的想法正确吗?或者Clang生成的代码在某些情况下可能会导致错误吗?


1
godbolt比较 https://gcc.godbolt.org/z/GFCEY3 - kpdev
8
似乎由于原子变量是局部变量,clang认识到只有一个线程可以访问它,并且避免生成任何关于原子变量的代码。 - Joachim Isaksson
2
GCC在核心语言级别上不支持原子操作,它们被视为库函数调用,类似于printf,永远不会被删除。而Clang生成了预期的代码。 - curiousguy
由于只有一个执行线程,因此 [tag:multithreading] 看起来并不相关,唯一的执行线程是主线程。 - curiousguy
1
也许如果您能解释为什么希望一个无意义的伪发布操作产生一个屏障,我们可以解释为什么这种直觉是错误的。向全世界发布一条消息,告诉他们你已经完成了某件事,并设置一个标志来说明。你要向谁射击,设置什么标志? - curiousguy
显示剩余4条评论
1个回答

7

一个更加实际的示例

#include <atomic>

std::atomic<int> a;

void foo_seq_cst(int b) {
    a = b;
}

void foo_relaxed(int b) {
    a.store(b, std::memory_order_relaxed);
}

gcc-9.1:

foo_seq_cst(int):
        mov     DWORD PTR a[rip], edi
        mfence
        ret
foo_relaxed(int):
        mov     DWORD PTR a[rip], edi
        ret

clang-8.0:

foo_seq_cst(int):                       # @foo_seq_cst(int)
        xchg    dword ptr [rip + a], edi
        ret
foo_relaxed(int):                       # @foo_relaxed(int)
        mov     dword ptr [rip + a], edi
        ret

gcc使用mfence,而clang在std::memory_order_seq_cst中使用xchg

xchg意味着lock前缀。 lockmfence都满足std::memory_order_seq_cst的要求,即无重排序和总顺序。

来自Intel 64和IA-32 Architectures软件开发人员手册:

MFENCE—Memory Fence

对于在MFENCE指令之前发布的所有从内存加载和存储到内存的指令执行串行化操作。 这种串行化操作保证了程序顺序中在MFENCE指令之前的每个负载和存储指令在之后的任何负载或存储指令之前在全局上可见。 MFENCE指令与所有负载和存储指令、其他MFENCE指令、任何LFENCE和SFENCE指令以及任何串行化指令(例如CPUID指令)相关。 MFENCE不对指令流进行串行化。

8.2.3.8 锁定指令具有完全顺序

内存排序模型确保所有处理器同意所有锁定指令的单个执行顺序,包括那些大于8字节或不自然对齐的锁定指令。

8.2.3.9 不会将加载和存储与锁定的指令重新排序

内存排序模型防止将在更早或更晚执行的锁定指令与加载和存储重新排序。

lock的基准测试速度比mfence快2-3倍,Linux 在可能的情况下从mfence切换到lock


1
gcc的bugzilla中是否有关于用lock替换mfence的条目? - Marc Glisse

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接