C11 Atomic Acquire/Release 和 x86_64 缺乏加载/存储一致性?

Chr*_*all 10 c x86-64 memory-model memory-barriers stdatomic

我正在努力处理 C11 标准的第 5.1.2.4 节,特别是发布/获取的语义。我注意到https://preshing.com/20120913/acquire-and-release-semantics/(以及其他)指出:

... 释放语义防止写入释放的内存重新排序与程序顺序之前的任何读取或写入操作。

因此,对于以下情况:

typedef struct test_struct
{
  _Atomic(bool) ready ;
  int  v1 ;
  int  v2 ;
} test_struct_t ;

extern void
test_init(test_struct_t* ts, int v1, int v2)
{
  ts->v1 = v1 ;
  ts->v2 = v2 ;
  atomic_store_explicit(&ts->ready, false, memory_order_release) ;
}

extern int
test_thread_1(test_struct_t* ts, int v2)
{
  int v1 ;
  while (atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
  ts->v2 = v2 ;       // expect read to happen before store/release 
  v1     = ts->v1 ;   // expect write to happen before store/release 
  atomic_store_explicit(&ts->ready, true, memory_order_release) ;
  return v1 ;
}

extern int
test_thread_2(test_struct_t* ts, int v1)
{
  int v2 ;
  while (!atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
  ts->v1 = v1 ;
  v2     = ts->v2 ;   // expect write to happen after store/release in thread "1"
  atomic_store_explicit(&ts->ready, false, memory_order_release) ;
  return v2 ;
}
Run Code Online (Sandbox Code Playgroud)

这些被执行的地方:

>   in the "main" thread:  test_struct_t ts ;
>                          test_init(&ts, 1, 2) ;
>                          start thread "2" which does: r2 = test_thread_2(&ts, 3) ;
>                          start thread "1" which does: r1 = test_thread_1(&ts, 4) ;
Run Code Online (Sandbox Code Playgroud)

因此,我希望线程“1”具有 r1 == 1,线程“2”具有 r2 = 4。

我期望是因为(遵循第 5.1.2.4 节的第 16 和 18 段):

  • 所有(非原子)读取和写入都是“先于顺序”,因此“发生在”线程“1”中的原子写入/释放之前,
  • 在线程“2”中“线程间发生之前”原子读取/获取(当它读取“真”时),
  • 这反过来又是“先排序”,因此“发生在”(非原子)读取和写入(在线程“2”中)之前。

然而,我完全有可能没有理解这个标准。

我观察到为 x86_64 生成的代码包括:

test_thread_1:
  movzbl (%rdi),%eax      -- atomic_load_explicit(&ts->ready, memory_order_acquire)
  test   $0x1,%al
  jne    <test_thread_1>  -- while is true
  mov    %esi,0x8(%rdi)   -- (W1) ts->v2 = v2
  mov    0x4(%rdi),%eax   -- (R1) v1     = ts->v1
  movb   $0x1,(%rdi)      -- (X1) atomic_store_explicit(&ts->ready, true, memory_order_release)
  retq   

test_thread_2:
  movzbl (%rdi),%eax      -- atomic_load_explicit(&ts->ready, memory_order_acquire)
  test   $0x1,%al
  je     <test_thread_2>  -- while is false
  mov    %esi,0x4(%rdi)   -- (W2) ts->v1 = v1
  mov    0x8(%rdi),%eax   -- (R2) v2     = ts->v2   
  movb   $0x0,(%rdi)      -- (X2) atomic_store_explicit(&ts->ready, false, memory_order_release)
  retq   
Run Code Online (Sandbox Code Playgroud)

提供了R1和X1发生的顺序,这给出结果我的期望。

但我对 x86_64 的理解是,读取顺序发生,其他读取和写入顺序发生,其他写入顺序发生,但读取和写入可能不会彼此顺序发生。这意味着 X1 有可能在 R1 之前发生,甚至 X1、X2、W2、R1 也有可能按此顺序发生——我相信。[这似乎极不可能,但如果 R​​1 被某些缓存问题阻止?]

请问:我有什么不明白的?

我注意到,如果我将加载/存储更改ts->readymemory_order_seq_cst,则为存储生成的代码是:

  xchg   %cl,(%rdi)
Run Code Online (Sandbox Code Playgroud)

这与我对 x86_64 的理解一致,并将给出我期望的结果。

Pet*_*des 1

x86 的内存模型基本上是顺序一致性加上存储缓冲区(具有存储转发)。所以每个商店都是一个发布商店1。这就是为什么只有 seq-cst 存储需要任何特殊指令。(C/C++11 原子映射到 asm)。此外,https://stackoverflow.com/tags/x86/info有一些 x86 文档的链接,包括x86-TSO 内存模型的正式描述(对于大多数人来说基本上不可读;需要费力地阅读大量定义)。

\n\n

既然您已经阅读了 Jeff Preshing 的优秀文章系列,我将向您指出另一篇更详细的文章:\n https://preshing.com/20120930/weak-vs-strong-内存模型/

\n\n

x86 上唯一允许的重新排序是 StoreLoad,而不是 LoadStore(如果我们用这些术语来讨论的话)。(如果加载仅部分重叠存储,则存储转发可以做额外有趣的事情;全局不可见加载指令,尽管您永远不会在编译器生成的代码中得到它stdatomic。)

\n\n

@EOF 评论了英特尔手册中的正确引用:

\n\n
\n

Intel\xc2\xae 64 和 IA-32 架构软件开发人员\xe2\x80\x99s 手册第 3 卷(3A、3B、3C 和 3D):系统编程指南,8.2.3.3早期加载时存储不会重新排序。

\n
\n\n
\n\n

脚注1:忽略弱有序NT存储;这就是为什么你通常sfence在做NT商店之后。C11 / C++11 实现假设您没有使用 NT 存储。如果是,请_mm_sfence在发布操作之前使用以确保它尊重您的 NT 存储。(一般情况下,在其他情况下不要使用_mm_mfence/_mm_sfence;通常您只需要阻止编译时重新排序。或者当然只使用 stdatomic。)

\n