我的印象是内存加载无法在C ++ 11内存模型中的获取负载之上提升。然而,看看gcc 4.8产生的代码,对其他原子载荷来说似乎只是真的,而不是所有的内存。如果这是真的并且获取负载不同步所有内存(只是std::atomics
)那么我不确定如何用std :: atomic实现通用互斥锁。
以下代码:
extern std::atomic<unsigned> seq;
extern std::atomic<int> data;
int reader() {
int data_copy;
unsigned seq0;
unsigned seq1;
do {
seq0 = seq.load(std::memory_order_acquire);
data_copy = data.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
seq1 = seq.load(std::memory_order_relaxed);
} while (seq0 != seq1);
return data_copy;
}
产地:
_Z6readerv:
.L3:
mov ecx, DWORD PTR seq[rip]
mov eax, DWORD PTR data[rip]
mov edx, DWORD PTR seq[rip]
cmp ecx, edx
jne .L3
rep ret
对我而言看起来是正确的。
但是,将数据更改为int
而不是std::atomic
:
extern std::atomic<unsigned> seq;
extern int data;
int reader() {
int data_copy;
unsigned seq0;
unsigned seq1;
do {
seq0 = seq.load(std::memory_order_acquire);
data_copy = data;
std::atomic_thread_fence(std::memory_order_acquire);
seq1 = seq.load(std::memory_order_relaxed);
} while (seq0 != seq1);
return data_copy;
}
产生这个:
_Z6readerv:
mov eax, DWORD PTR data[rip]
.L3:
mov ecx, DWORD PTR seq[rip]
mov edx, DWORD PTR seq[rip]
cmp ecx, edx
jne .L3
rep ret
那是怎么回事?
答案 0 :(得分:4)
为什么在获取
之上悬挂负载我已在gcc bugzilla发布此内容,他们已将其确认为错误。
MEM alias-set为-1(ALIAS_SET_MEMORY_BARRIER)应该阻止这个, 但PRE不知道这个特殊属性(它应该“杀死”所有的参考 越过它。)
看起来gcc wiki有一个很好的页面。
通常,发布是下沉代码的障碍,获取是提升代码的障碍。
为什么此代码仍然存在
根据this paper,我的代码仍然不正确,因为它引入了数据竞争。即使修补后的gcc生成了正确的代码,如果不将data
包装在std::atomic
中,仍然无法访问int foo(unsigned x) {
if (x < 10) {
/* some calculations that spill all the
registers so x has to be reloaded below */
switch (x) {
case 0:
return 5;
case 1:
return 10;
// ...
case 9:
return 43;
}
}
return 0;
}
。原因是数据争用是未定义的行为,即使由它们产生的计算被丢弃。
AdamH.Peterson提供的一个例子:
{{1}}
这里编译器可能会将切换器优化为跳转表,并且由于上面的if语句可以避免范围检查。但是,如果数据争用不是未定义的行为,则需要进行第二次范围检查。
答案 1 :(得分:1)
我认为您的atomic_thread_fence不正确。与代码一起使用的唯一C ++ 11内存模型是seq_cst。但这非常昂贵(你将得到一个完整的记忆围栏)以满足你的需要。
原始代码有效,我认为这是性能最佳的权衡。
根据您的更新编辑:
如果您正在寻找具有常规int的代码无法按照您喜欢的方式工作的正式原因,我相信您引用的论文(http://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf)给出了答案。请看第2节的结尾。您的代码与图1中的代码具有相同的问题。它有数据竞争。多个线程可以同时对常规int上的同一内存执行操作。它被c ++ 11内存模型禁止,这段代码正式无效的C ++代码。
gcc希望代码没有数据竞争,即有效的C ++代码。由于没有竞争且代码无条件地加载int,因此可以在函数体中的任何位置发出负载。所以gcc很聪明,它只是发出一次,因为它不易变。通常与获取障碍密切相关的条件语句在编译器的作用中起着重要作用。
在标准的正式俚语中,原子载荷和常规int载荷未被排序。例如,条件的引入将创建一个序列点,并强制编译器在序列点(http://msdn.microsoft.com/en-us/library/d45c7a5d.aspx)之后计算常规int。然后c ++内存模型将完成剩下的工作(即确保执行指令的cpu的可见性)
所以你的陈述都不是真的。你绝对可以使用c ++ 11构建一个锁,而不是一个有数据竞争的锁:-)通常一个锁会在读之前等待(这显然是你在这里要避免的)所以你没有这种问题。
请注意,您的原始seqlock是错误的,因为您不想只检查seq0!= seq1(您可能正处于更新中)。 seqlock纸张具有正确的条件。
答案 2 :(得分:0)
我仍然在推理这些非顺序一致的内存顺序操作和障碍,但可能是这个代码生成是正确的(或者说是允许的)。从表面上看,它看起来确实很可疑,但如果没有办法让符合标准的程序告诉数据的负载被提升(这意味着这个代码在“好像”下是正确的话)我不会感到惊讶“规则”。
程序正在从原子读取两个后续值,一个在加载之前,一个在加载之后,并且只要它们不匹配就重新发出加载。原则上,没有理由两个原子读取必须看到彼此不同的值。即使刚刚发生原子写入,该线程也无法检测到它没有再次读取旧值。然后该线程将返回循环并最终从原子读取两个一致的值,然后返回,但由于seq0
和seq1
被丢弃,程序无法告诉seq0
和data
中的值{1}}与从data
读取的值不对应。现在,原则上,这也告诉我整个循环可能已被省略,只有reader()
的负载实际上是正确性所必需的,但是不能忽略循环不一定是正确性问题。
如果pair<int,unsigned>
要返回包含seq0
(或seq1
)的{{1}}并生成相同的提升循环,我认为这可能是错误的代码(但同样我是这种非顺序一致的操作推理的新手。)