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 段):
- 所有(非原子)读取和写入都是 "sequenced before",因此 "happen before" 线程“1”中的原子 write/release,
- 其中 "inter-thread-happens-before" 线程“2”中的原子 read/acquire(当它读取 'true' 时),
- 这又是 "sequenced before",因此 "happens before"(非原子)读写(在线程“2”中)。
不过,完全有可能是我没看懂标准。
我观察到为 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 也有可能按此顺序发生——我相信。 [这似乎极不可能,但如果 R1 被某些缓存问题阻止了?]
请问:我哪里不明白?
我注意到,如果我将 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。)
我正在努力研究 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 段):
- 所有(非原子)读取和写入都是 "sequenced before",因此 "happen before" 线程“1”中的原子 write/release,
- 其中 "inter-thread-happens-before" 线程“2”中的原子 read/acquire(当它读取 'true' 时),
- 这又是 "sequenced before",因此 "happens before"(非原子)读写(在线程“2”中)。
不过,完全有可能是我没看懂标准。
我观察到为 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 也有可能按此顺序发生——我相信。 [这似乎极不可能,但如果 R1 被某些缓存问题阻止了?]
请问:我哪里不明白?
我注意到,如果我将 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。)