x86 mfence和C ++内存屏障

Adv*_*ere 4 x86 gcc memory-model memory-barriers c++11

我正在检查编译器如何为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();
}
Run Code Online (Sandbox Code Playgroud)

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

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

MFENCE不使用的情况下,如何确保将存储缓冲区数据提交给高速缓存以确保内存顺序语义?

以下是使用get / set函数的汇编代码-O3,例如我们在Godbolt编译器资源管理器中获得的代码

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
Run Code Online (Sandbox Code Playgroud)

Had*_*ais 7

x86内存排序模型为所有存储指令1提供了#StoreStore和#LoadStore障碍,这就是发行语义所需要的。另外,处理器将尽快提交存储指令。当存储指令退出时,存储在存储缓冲区中成为最旧的存储,内核使目标高速缓存行处于可写一致性状态,并且高速缓存端口可用于执行存储操作2。因此,无需MFENCE指示。该标志将尽快对另一个线程可见,并且在出现该标志时,请any_value确保该标志为10。

另一方面,顺序一致性还需要#StoreLoad和#LoadLoad障碍。MFENCE需要同时提供3个障碍,因此可以在所有优化级别上使用。

相关:英特尔硬件上的存储缓冲区大小?什么是存储缓冲区?


脚注:

(1)有些例外情况不适用于这里。特别是,非临时存储和存储到不可缓存的写合并内存类型仅提供#LoadStore障碍。无论如何,这些障碍为在Intel和AMD处理器上存储回写式存储器类型提供了条件。

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

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


Pet*_*des 6

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

(请参阅注释中的讨论:“是否允许StoreLoad重新排序”是否是x86所允许的准确和充分的描述。内核总是按程序顺序查看其自己的存储,因为加载会监听存储缓冲区,因此您可以说存储转发也重新排序最近存储的数据的加载。但您不能总是这样:全局不可见的加载说明

(和BTW,编译器比GCC使用其他xchg做SEQ-CST店。这实际上是对当前的CPU效率。GCC的mov+ mfence可能已经过去便宜,但目前通常更糟,即使你不关心旧值。请参阅为什么具有顺序一致性的std :: atomic存储区使用XCHG?进行GCC mov+mfencexchg。之间的比较。另外,我的回答是在x86哪一个更好的写障碍:lock + addl或xchgl?

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

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


当我在没有优化的情况下使用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因为它是如此昂贵,但这不是我们得到的。

  • @PeterCordes - 当然,我们只是假设“ad”是寄存器,然后将它们打印出来,本身没有观察,它们只是本地状态(寄存器),并且可以在没有任何并发​​问题的情况下公开它们。总的来说,我认为这与这里无关,我只是对与观察负载的其他线程相关的 StoreLoad 的描述感到困惑。StoreLoad 通常是纯本地的。`TSO = seqcst + 存储缓冲区` - 我认为这还不够。您需要`seqcst +存储缓冲区**和**存储转发`,因为在没有存储转发的系统上,我展示的石蕊测试是被禁止的。 (2认同)
  • ...但我很确定现代 x86 实际上可以自由地将负载移动到存储之前,甚至除了缓冲之外:例如,甚至在存储执行之前执行负载(我不是在谈论不允许的但让尝试一下) -此处 MOB 检测到的推测性排序)。因此,硬件导致了正式模型中的规则,但后来的硬件不仅限于原始硬件模型的行为,它还可以以其他方式使用允许的重新排序。 (2认同)

归档时间:

查看次数:

470 次

最近记录:

7 年,2 月 前