x86 mfence和C ++内存屏障

问题描述 投票:4回答:2

我正在检查编译器如何在x86_64上发出多核内存屏障的指令。下面的代码是我正在使用gcc_x86_64_8.3测试的代码。

std::atomic<bool> flag {false};
int any_value {0};

void set()
{
  any_value = 10;
  flag.store(true, std::memory_order_release);
}

void get()
{
  while (!flag.load(std::memory_order_acquire));
  assert(any_value == 10);
}

int main()
{
  std::thread a {set};
  get();
  a.join();
}

当我使用std::memory_order_seq_cst时,我可以看到MFENCE指令与任何优化-O1, -O2, -O3一起使用。此指令确保刷新存储缓冲区,因此在L1D缓存中更新其数据(并使用MESI协议确保其他线程可以看到效果)。

但是当我使用没有优化的std::memory_order_release/acquire时,也会使用MFENCE指令,但是使用-O1, -O2, -O3优化省略了指令,并且没有看到其他指令刷新缓冲区。

在没有使用MFENCE的情况下,是什么确保存储缓冲区数据被提交到缓存内存以确保内存顺序语义?

下面是使用-O3的get / set函数的汇编代码,就像我们得到的on the Godbolt compiler explorer

set():
        mov     DWORD PTR any_value[rip], 10
        mov     BYTE PTR flag[rip], 1
        ret


.LC0:
        .string "/tmp/compiler-explorer-compiler119218-62-hw8j86.n2ft/example.cpp"
.LC1:
        .string "any_value == 10"

get():
.L8:
        movzx   eax, BYTE PTR flag[rip]
        test    al, al
        je      .L8
        cmp     DWORD PTR any_value[rip], 10
        jne     .L15
        ret
.L15:
        push    rax
        mov     ecx, OFFSET FLAT:get()::__PRETTY_FUNCTION__
        mov     edx, 17
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:.LC1
        call    __assert_fail
c++11 gcc x86 memory-barriers memory-model
2个回答
7
投票

x86内存排序模型为所有存储指令1提供了#StoreStore和#LoadStore屏障,这就是发布语义所需要的。处理器也将尽快提交存储指令;当存储指令退出时,存储变为存储缓冲器中最旧的存储,核心具有处于可写相干状态的目标高速缓存行,并且高速缓存端口可用于执行存储操作2。所以不需要MFENCE指令。该标志将尽快显示给另一个线程,当它出现时,any_value保证为10。

另一方面,顺序一致性也需要#StoreLoad和#LoadLoad障碍。 MFENCE需要提供3个障碍,因此它可用于所有优化级别。

相关:Size of store buffers on Intel hardware? What exactly is a store buffer?


脚注:

(1)有些例外情况不适用于此。特别是,对不可缓存的写入组合存储器类型的非临时存储和存储仅提供#LoadStore屏障。无论如何,这些障碍是为英特尔和AMD处理器上的回写存储器类型的商店提供的。

(2)这与在某些条件下全局可见的写组合存储形成对比。请参见英特尔手册第3卷第11.3.1节。

(3)参见彼得答案下的讨论。


6
投票

x86的TSO内存模型是顺序一致性+存储缓冲区,因此只有seq-cst存储需要任何特殊的防护。 (在存储缓冲区耗尽之后,在稍后加载之前停止,我们需要恢复顺序一致性)。较弱的acq / rel模型与由存储缓冲区引起的StoreLoad重新排序兼容。

(参见评论中的讨论:是否“允许StoreLoad重新排序”是对x86允许的内容的准确和充分的描述。核心总是按程序顺序看到自己的存储,因为加载窥探了存储缓冲区,所以你可以说存储转发也是重新排序最近存储的数据的负载。除了你不能总是:Globally Invisible load instructions

(顺便说一句,除了gcc之外的编译器使用xchg来做一个seq-cst商店。这实际上对当前的CPU更有效。过去GCC的mov + mfence可能更便宜,但即使你不这样做,目前通常也会更糟关心旧的价值。请参阅Why does a std::atomic store with sequential consistency use XCHG?,了解GCC的mov+mfencexchg之间的比较。另外我对Which is a better write barrier on x86: lock+addl or xchgl?的回答)

有趣的事实:您可以通过隔离seq-cst加载而不是存储来实现顺序一致性。但是对于大多数用例而言,便宜货物比廉价商店更有价值,所以每个人都使用ABI,商店里有完全障碍。

有关C ++ 11原子操作如何映射到x86,PowerPC,ARMv7,ARMv8和Itanium的asm指令序列的详细信息,请参阅https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html。还有When are x86 LFENCE, SFENCE and MFENCE instructions required?


当我使用std :: memory_order_release / acquire而没有优化时,也使用了MFENCE指令

这是因为flag.store(true, std::memory_order_release);没有内联,因为你禁用了优化。这包括内联非常简单的成员函数,如``atomic :: store(T,std :: memory_order = std :: memory_order_seq_cst)`

__atomic_store_n() GCC内置的排序参数是运行时变量时(在atomic::store()库实现中),GCC将其保守并将其提升为seq_cst。实际上值得让gcc分支mfence,因为它太贵了,但这不是我们得到的。

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