C11原子获取/发布和x86_64缺乏加载/存储一致性?


10

我在C11标准的5.1.2.4节中苦苦挣扎,尤其是Release / Acquire的语义。我注意到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 ;
}

这些在哪里执行:

>   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段):

  • 所有(非原子的)读取和写入都是“先于顺序”的,因此是在线程“ 1”中的原子写入/释放的“发生之前”,
  • 哪个在线程“ 2”中“先于线程间发生”,(当它读为“ true”时),
  • 它依次(在原子“ 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   

假设 R1和X1以此顺序发生,这给出了我期望的结果。

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

请:我不明白什么?

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

  xchg   %cl,(%rdi)

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


5
在x86上,所有普通(非临时)存储都具有发布语义。英特尔®64和IA-32体系结构软件开发人员手册第3卷(3A,3B,3C和3D):系统编程指南8.2.3.3 Stores Are Not Reordered With Earlier Loads。因此,您的编译器正确地翻译了您的代码(多么令人惊奇),以使您的代码有效地完全顺序执行,并且没有任何有趣的事情同时发生。
EOF

谢谢 !(我正悄悄地玩弄。)FWIW我建议链接 -特别是第3节“程序员模型”。但是要避免我犯的错误,请注意,在“ 3.1抽象机”中有“硬件线程”,每个线程都是“ 指令执行的单个顺序流”(我特别强调)。我现在可以重新尝试了解C11标准...,减少认知上的不和谐:-)
克里斯·霍尔

Answers:


1

x86的内存模型基本上是顺序一致性加存储缓冲区(带有存储转发)的。因此,每个商店都是发行商店1。这就是为什么只有seq-cst存储区需要任何特殊说明的原因。(C / C ++ 11原子映射到asm)。另外,https://stackoverflow.com/tags/x86/info包含一些指向x86文档的链接,包括对x86-TSO内存模型的正式描述(对于大多数人来说基本上是不可读的;需要花很多时间才能得出结论)。

由于您已经阅读了Jeff Preshing的精彩文章系列,因此,我为您指出了另一篇更详细的文章:https : //preshing.com/20120930/weak-vs-strong-memory-models/

如果我们使用这些术语,则在x86上唯一允许的重新排序是StoreLoad,而不是LoadStore。(如果加载仅与商店部分重叠,则存储转发会带来更多有趣的事情;全局看不见的加载指令,尽管您永远不会在编译器生成的代码中获得该信息stdatomic。)

@EOF引用了英特尔手册中的正确报价:

英特尔®64和IA-32体系结构软件开发人员手册第3卷(3A,3B,3C和3D):系统编程指南8.2.3.3存储库未按较早的加载顺序进行排序。


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


我发现x86-TSO:x86多处理器的严格且可用的程序员模型比您所引用的(相关)形式描述更具可读性。但是我的真正抱负是要完全理解C11 / C18标准的5.1.2.4和7.17.3节。特别是,我想我会获得Release / Acquire / Acquire + Release,但是memory_order_seq_cst是单独定义的,我正在努力查看它们如何融合在一起:-(
Chris Hall

@ChrisHall:我发现它有助于准确地了解acq / rel有多弱,为此,您需要查看可以进行IRIW重排序的POWER等机器。(其中seq-cst禁止,但acq / rel不禁止)。 是否会在其他线程中始终以相同的顺序看到对不同线程中不同位置的两次原子写操作?。还如何在C ++ 11中实现StoreLoad障碍?关于标准在形式上与synchronize-with或everything-seq-cst之外的订单的形式保证多少,有一些讨论。
彼得·科德斯

@ChrisHall:seq-cst的主要作用是阻止StoreLoad重新排序。(在x86上,这是acq / rel以外唯一的功能)。 preshing.com/20120515/memory-reordering-in-act-in-act使用asm,但等效于seq-cst vs. acq / rel
Peter Cordes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.