我的印象是,在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那到底是怎么回事?
发布于 2013-05-30 04:25:59
为什么负载被提升到acquire上方
我已经在gcc bugzilla上发布了这篇文章,他们已经确认这是一个bug。
内存别名集-1 (ALIAS_SET_MEMORY_BARRIER)应该可以防止这种情况发生,但是并不知道这个特殊的属性(它应该“杀死”所有通过它的引用)。
看起来gcc wiki在这方面有一个很好的页面。
通常,发布是下沉代码的障碍,获取是提升代码的障碍。
为什么这段代码仍然是坏的
根据this paper,我的代码仍然是不正确的,因为它引入了数据竞争。即使打了补丁的gcc生成了正确的代码,但如果不将其包装在std::atomic中,则访问data仍然是不合适的。原因是数据竞争是未定义的行为,即使由它们产生的计算被丢弃。
由AdamH.Peterson提供的一个示例:
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;
}在这里,编译器可能会将切换优化为跳转表,多亏了上面的if语句,才能避免范围检查。然而,如果数据竞争不是未定义的行为,那么将需要第二次范围检查。
发布于 2013-05-24 05:04:29
我认为您的atomic_thread_fence不正确。与您的代码一起工作的唯一C++11内存模型是seq_cst one。但是这对于你需要的东西来说是非常昂贵的(你将得到一个完整的内存栅栏)。
原始代码可以工作,我认为这是最佳的性能折衷。
根据您的更新进行编辑:
如果您正在寻找使用常规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++内存模型将完成其余工作(即通过执行指令来确保可见性)。
所以你的说法都不是真的。你绝对可以用c++11构建一个锁,但不是一个数据竞争的锁:-)通常一个锁需要在读取之前等待(这显然是你在这里试图避免的),所以你不会有这种问题。
请注意,您的原始seqlock是错误的,因为您不想只检查seq0 != seq1 (您可能正在进行更新)。序列纸的状态正确。
发布于 2013-05-24 02:47:03
对于这些非顺序一致的内存顺序操作和障碍,我还是个新手,但可能是这种代码生成是正确的(或者更确切地说是允许的)。从表面上看,它看起来确实很可疑,但如果符合标准的程序没有办法判断来自数据的加载已被提升,我也不会感到惊讶(这意味着在“好像”规则下,这段代码是正确的)。
该程序从原子读取两个后续值,一个在加载之前,一个在加载之后,并在它们不匹配时重新发出加载。原则上,两个原子读取没有理由看到彼此不同的值。即使原子写入刚刚发生,这个线程也无法检测到它没有再次读取旧值。然后线程将返回到循环,并最终从原子读取两个一致的值,然后返回,但由于seq0和seq1随后被丢弃,程序无法判断seq0中的值与从data读取的值不对应。现在,原则上,这也向我表明,整个循环可以被省略,只有来自data的加载实际上是正确性所必需的,但未能省略循环并不一定是正确性问题。
如果reader()返回一个包含seq0 (或seq1)的pair<int,unsigned>,并且生成了相同的提升循环,我认为它可能是不正确的代码(但我还是第一次使用这种非顺序一致的操作推理)。
https://stackoverflow.com/questions/16718799
复制相似问题