x86 mfence和C ++内存屏障

时间:2019-03-18 23:42:53

标签: c++11 gcc x86 memory-barriers memory-model

我正在检查编译器如何发出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

2 个答案:

答案 0 :(得分: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障碍。无论如何,这些障碍为在Intel和AMD处理器上存储回写式存储器类型提供了条件。

(2)与在某些条件下全局可见的写合并存储相反。请参阅英特尔手册第3卷的11.3.1节。

(3)参见Peter的回答。

答案 1 :(得分:6)

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

(请参阅注释中的讨论,re:“允许StoreLoad重新排序”是否是x86所允许的准确和充分的描述。内核总是按程序顺序查看其自己的存储,因为加载会监听存储缓冲区,因此您可以说该存储-forwarding还会对最近存储的数据进行重新排序。除非您不能总是:Globally Invisible load instructions

(顺便说一句,而且,gcc以外的编译器使用xchg来进行seq-cst存储。实际上,在当前CPU上,效率更高了[em> 。GCC的mov + {{ 1}}过去可能比较便宜,但即使您不关心旧值,目前通常也会更糟。请参阅Why does a std::atomic store with sequential consistency use XCHG?以了解GCC mfence与{{1} }。还有我对Which is a better write barrier on x86: lock+addl or xchgl?的回答

有趣的事实:您可以通过代替seq-cst loads 而不是存储来实现顺序一致性。但是对于大多数用例而言,廉价负载比廉价商店有价值得多,因此每个人都使用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指令

这是因为mov+mfence不内联,因为您禁用了优化。这包括内联非常简单的成员函数,例如``atomic :: store(T,std :: memory_order = std :: memory_order_seq_cst)`

当内置xchg GCC的排序参数是运行时变量(在flag.store(true, std::memory_order_release);库实现中)时, GCC保守地将其播放并提升为seq_cst。实际上,gcc分支到__atomic_store_n()上可能是值得的,因为它是如此昂贵,但这不是我们得到的。