C11 原子 Acquire/Release 和 x86_64 缺乏 load/store 连贯性?

C11 Atomic Acquire/Release and x86_64 lack of load/store coherence?

我正在努力研究 C11 标准的第 5.1.2.4 节,尤其是 Release/Acquire 的语义。我注意到 https://preshing.com/20120913/acquire-and-release-semantics/(除其他外)指出:

... Release semantics prevent memory reordering of the write-release with any read or write operation that precedes it in program order.

因此,对于以下内容:

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 ;
}

执行地点:

>   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) ;

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

我希望如此,因为(根据第 5.1.2.4 节的第 16 和 18 段):

不过,完全有可能是我没看懂标准。

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

test_thread_1:
  movzbl (%rdi),%eax      -- atomic_load_explicit(&ts->ready, memory_order_acquire)
  test   [=15=]x1,%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   [=15=]x1,(%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   [=15=]x1,%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   [=15=]x0,(%rdi)      -- (X2) atomic_store_explicit(&ts->ready, false, memory_order_release)
  retq   

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

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

请问:我哪里不明白?

我注意到,如果我将 ts->ready 的 loads/stores 更改为 memory_order_seq_cst,则为商店生成的代码是:

  xchg   %cl,(%rdi)

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

x86 的内存模型基本上是顺序一致性加上存储缓冲区(带存储转发)。所以每个store都是一个release-store1。这就是为什么只有 seq-cst 存储需要任何特殊说明。 (C/C++11 atomics mappings to asm). Also, https://whosebug.com/tags/x86/info has some links to x86 docs, including a formal description of the x86-TSO memory model(对于大多数人来说基本上是不可读的;需要费力地研究很多定义)。

既然您已经阅读了 Jeff Preshing 的优秀系列文章,我将向您介绍另一篇更详细的文章: https://preshing.com/20120930/weak-vs-strong-memory-models/

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

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

Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B, 3C & 3D): System Programming Guide, 8.2.3.3 Stores Are Not Reordered With Earlier Loads.


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