通过获取原子载荷提升非原子载荷
Hoisting of non-atomic loads up through acquiring atomic loads
我的印象是,在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
那到底发生了什么?
我已经在gcc bugzilla上发布了这个消息,他们已经确认它是一个bug。
MEM别名集-1(alias_set_MEMORY_BARRIER(被认为是为了防止这种情况,但是PRE不知道这个特殊的属性(它应该"杀死"所有的裁判穿过它(。
看起来gcc wiki有一个很好的页面。
一般来说,发布是下沉代码的障碍,获取是提升代码的障碍。
为什么此代码仍然被破坏
根据本文,我的代码仍然不正确,因为它引入了数据竞赛。即使打了补丁的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语句,可以避免范围检查。然而,如果数据竞赛不是未定义的行为,则需要进行第二次范围检查。
我认为您的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++代码。由于没有race,并且代码无条件地加载int,因此可以在函数体的任何位置发出加载。所以gcc是智能的,它只发出一次,因为它不易失。通常与获取障碍并行的条件语句在编译器的操作中起着重要作用
在标准的正式俚语中,原子负载和常规int负载是不排序的。例如,条件的引入将创建一个序列点,并迫使编译器在序列点之后计算正则int(http://msdn.microsoft.com/en-us/library/d45c7a5d.aspx)。然后,c++内存模型将完成剩下的工作(即通过执行指令的cpu确保可见性(
所以你的说法都不是真的。你肯定可以用c++11构建一个锁,只是不能用数据竞赛的锁:-(通常情况下,锁需要在读取之前等待(这显然是你在这里试图避免的(,所以你不会遇到这种问题。
请注意,您原来的seqlock有缺陷,因为您不想只检查seq0!=seq1(您可能正在进行更新(。seqlock纸张具有正确的条件。
我对这些非顺序一致的内存顺序操作和障碍的推理还是个新手,但可能是这种代码生成是正确的(或者说是允许的(。从表面上看,这看起来确实很可疑,但如果一个符合标准的程序无法判断数据中的负载已被提升,我也不会感到惊讶(这意味着根据"假设"规则,该代码是正确的(。
该程序从原子中读取两个后续值,一个在加载之前,一个是在加载之后,并在它们不匹配时重新发出加载。原则上,的两个原子读取不需要看到彼此不同的值。即使刚刚发生了原子写入,这个线程也无法检测到它没有再次读取旧值。然后,线程将返回到循环中,并最终从原子中读取两个一致的值,然后返回,但由于seq0
和seq1
随后被丢弃,因此程序无法判断seq0
中的值与从data
中读取的值不对应。现在,原则上,这也向我表明,整个循环本可以被消除,只有来自data
的负载才是正确性所必需的,但未能消除循环并不一定是正确性问题。
如果reader()
返回一个包含seq0
(或seq1
(的pair<int,unsigned>
,并且生成了相同的提升循环,我认为这可能是不正确的代码(但我对这种非顺序一致的操作推理还是新手(。
- C++为构建时间获取QDateTime的可靠方法
- lambda参数转换为constexpr技巧,然后获取带链接的数组
- 如何使用 < 和 > 命令获取 c++ 中的输入和输出?
- 使用指针从C++中的数组中获取最大值
- 如何获取std::result_of函数的返回类型
- 如何在openssl-ecc中获取十六进制格式的私钥
- 使用Unreal C++获取VR耳机的世界位置/方向
- 获取日期异步信号安全吗?如果在信号处理程序中使用,它会导致死锁吗
- 从C字符串中获取奇怪的字符串长度
- 为什么我的for循环不能正确获取argv
- 从python中调用C++函数并获取返回值
- 如何获取一个数字的前3位
- 获取字符串的长度并将其分配给数组
- 无法获取菜单选择以运行函数.C++
- 数组长度,为什么从命令行获取时不能使用它?
- Boost Spirit,获取迭代器内部语义动作
- 尝试通过OCI例程从Oracle获取blob数据,但出现错误:ORA-01008:并非所有变量都绑定
- 具有默认值的引用获取函数
- xmake总是报告:错误:无法获取cxx的程序,为什么
- 在c++中获取大文件的大小