C++11 C++;11关于现代英特尔:我疯了还是非原子对齐的64位加载/存储实际上是原子的?
我是否可以将一个任务关键型应用程序建立在这个测试结果的基础上,即100个线程读取一个主线程10亿次设置的指针时,永远不会看到一个撕裂 除了撕裂,还有其他潜在的问题吗 下面是一个独立的演示,它使用C++11 C++;11关于现代英特尔:我疯了还是非原子对齐的64位加载/存储实际上是原子的?,c++11,c++14,c++17,stdthread,stdatomic,C++11,C++14,C++17,Stdthread,Stdatomic,我是否可以将一个任务关键型应用程序建立在这个测试结果的基础上,即100个线程读取一个主线程10亿次设置的指针时,永远不会看到一个撕裂 除了撕裂,还有其他潜在的问题吗 下面是一个独立的演示,它使用g++-g tear.cxx-o tear-pthread编译 #include <atomic> #include <thread> #include <vector> using namespace std; void* pvTearTest; atomic&l
g++-g tear.cxx-o tear-pthread
编译
#include <atomic>
#include <thread>
#include <vector>
using namespace std;
void* pvTearTest;
atomic<int> iTears( 0 );
void TearTest( void ) {
while (1) {
void* pv = (void*) pvTearTest;
intptr_t i = (intptr_t) pv;
if ( ( i >> 32 ) != ( i & 0xFFFFFFFF ) ) {
printf( "tear: pv = %p\n", pv );
iTears++;
}
if ( ( i >> 32 ) == 999999999 )
break;
}
}
int main( int argc, char** argv ) {
printf( "\n\nTEAR TEST: are normal pointer read/writes atomic?\n" );
vector<thread> athr;
// Create lots of threads and have them do the test simultaneously.
for ( int i = 0; i < 100; i++ )
athr.emplace_back( TearTest );
for ( int i = 0; i < 1000000000; i++ )
pvTearTest = (void*) (intptr_t)
( ( i % (1L<<32) ) * 0x100000001 );
for ( auto& thr: athr )
thr.join();
if ( iTears )
printf( "%d tears\n", iTears.load() );
else
printf( "\n\nTEAR TEST: SUCCESS, no tears\n" );
}
函数TearTest()的汇编程序代码转储:
0x0000000000401256:推送%rbp
0x0000000000401257:mov%rsp,%rbp
0x000000000040125a:子$0x10,%rsp
0x000000000040125e:movq$0x0,-0x8(%rbp)
0x0000000000401266:movzbl 0x6e83(%rip),%eax#0x4080f0
0x000000000040126d:测试%al,%al
0x000000000040126f:jne 0x40130c
=>0x0000000000401275:mov$0x4080d8,%edi
0x000000000040127a:callq 0x40193a
0x000000000040127f:mov%rax,-0x10(%rbp)
0x0000000000401283:mov-0x10(%rbp),%rax
0x0000000000401287:sar$0x20,%rax
0x000000000040128b:mov-0x10(%rbp),%rdx
0x000000000040128f:mov%edx,%edx
0x0000000000401291:cmp%rdx,%rax
0x0000000000401294:je 0x4012bb
0x0000000000401296:mov-0x10(%rbp),%rax
0x000000000040129a:mov%rax,%rsi
0x000000000040129d:mov$0x40401a,%edi
0x00000000004012a2:mov$0x0,%eax
0x00000000004012a7:callq 0x401040
0x00000000004012ac:mov$0x0,%esi
0x00000000004012b1:mov$0x4080e0,%edi
0x00000000004012b6:callq 0x401954
0x00000000004012bb:mov-0x8(%rbp),%rax
0x00000000004012bf:lea 0x1(%rax),%rcx
0x00000000004012c3:movabs$0xabcc77118461cefd,%rdx
0x00000000004012cd:mov%rcx,%rax
0x00000000004012d0:mul%rdx
0x00000000004012d3:mov%rdx,%rax
0x00000000004012d6:shr$0x19,%rax
0x00000000004012da:imul$0x2faf080,%rax,%rax
0x00000000004012e1:子%rax,%rcx
0x00000000004012e4:mov%rcx,%rax
0x00000000004012e7:测试%rax,%rax
0x00000000004012ea:jne 0x401302
0x00000000004012ec:mov-0x10(%rbp),%rax
0x00000000004012f0:mov%rax,%rsi
0x00000000004012f3:mov$0x40402a,%edi
0x00000000004012f8:mov$0x0,%eax
0x00000000004012fd:callq 0x401040
0x0000000000401302:addq$0x1,-0x8(%rbp)
0x0000000000401307:jmpq 0x401266
0x000000000040130c:mov-0x8(%rbp),%rax
0x0000000000401310:mov%rax,%rsi
0x0000000000401313:mov$0x4080e8,%edi
0x0000000000401318:callq 0x401984
0x000000000040131d:否
0x000000000040131e:LEVEQ
0x00000000004013F:retq
是的,在x86上,对齐的负载是原子的,但是这是一个您不应该依赖的架构细节 <>因为你正在编写C++代码,你必须遵守C++标准的规则,即,你必须使用原子,而不是易失性。事实 在引入之前很久,
volatile
就已经是该语言的一部分了
C++11中线程的数量应该足够强烈地表明volatile
是
从未设计或打算用于多线程。重要的是
注意,C++中的代码>易失性<代码>与“代码> Value< /代码>有本质区别。
在Java或C等语言中(在这些语言中,volatile
处于
事实与内存模型相关,因此更像是C++中的原子模型)
在C++中,<>代码> Value用于通常被称为“异常内存”的操作。
这通常是可以在当前进程之外读取或修改的内存,
例如,当使用内存映射I/O时。volatile
强制编译器
按照指定的确切顺序执行所有操作。这防止了
一些对原子学来说完全合法的优化,同时也允许
有些优化对原子学来说实际上是非法的。例如:
volatile int x;
int y;
volatile int z;
x = 1;
y = 2;
z = 3;
z = 4;
...
int a = x;
int b = x;
int c = y;
int d = z;
在本例中,对z
有两个赋值,对x
有两个读取操作。
如果x
和z
是原子而不是volatile,编译器可以自由地处理
第一个存储区是不相关的,只需将其删除即可。同样,它也可以重用
第一次加载x
时返回的值,有效地生成类似int b=a
的代码。
但是由于x
和z
是易变的,所以这些优化是不可能的。相反
编译器必须确保所有易失性操作都在
指定的精确顺序,即,不稳定操作不能使用
相互尊重。但是,这并不阻止编译器重新排序
非易失性操作。例如,y
上的操作可以自由移动
向上或向下-如果x
和z
是原子,这是不可能的。所以
如果要尝试基于易失性变量实现锁,编译器
可以简单地(合法地)将一些代码移到关键部分之外
最后但并非最不重要的一点是,应注意将变量标记为volatile
不会阻止它参与数据竞赛。在那些罕见的情况下
拥有一些“不寻常的内存”(因此确实需要易失性)
同样由多个线程访问,您必须使用volatile原子
由于对齐的加载实际上是x86上的原子加载,编译器将把atomic.load()
调用转换为简单的mov
指令,因此原子加载并不比读取易失性变量慢。atomic.store()
实际上比编写volatile变量慢,但这是有充分理由的,因为与volatile写入相比,它在默认情况下是顺序一致的。你可以放松记忆顺序,但你必须知道
Dump of assembler code for function TearTest():
0x0000000000401256 <+0>: push %rbp
0x0000000000401257 <+1>: mov %rsp,%rbp
0x000000000040125a <+4>: sub $0x10,%rsp
0x000000000040125e <+8>: movq $0x0,-0x8(%rbp)
0x0000000000401266 <+16>: movzbl 0x6e83(%rip),%eax # 0x4080f0 <bEnd>
0x000000000040126d <+23>: test %al,%al
0x000000000040126f <+25>: jne 0x40130c <TearTest()+182>
=> 0x0000000000401275 <+31>: mov $0x4080d8,%edi
0x000000000040127a <+36>: callq 0x40193a <std::atomic<void*>::operator void*() const>
0x000000000040127f <+41>: mov %rax,-0x10(%rbp)
0x0000000000401283 <+45>: mov -0x10(%rbp),%rax
0x0000000000401287 <+49>: sar $0x20,%rax
0x000000000040128b <+53>: mov -0x10(%rbp),%rdx
0x000000000040128f <+57>: mov %edx,%edx
0x0000000000401291 <+59>: cmp %rdx,%rax
0x0000000000401294 <+62>: je 0x4012bb <TearTest()+101>
0x0000000000401296 <+64>: mov -0x10(%rbp),%rax
0x000000000040129a <+68>: mov %rax,%rsi
0x000000000040129d <+71>: mov $0x40401a,%edi
0x00000000004012a2 <+76>: mov $0x0,%eax
0x00000000004012a7 <+81>: callq 0x401040 <printf@plt>
0x00000000004012ac <+86>: mov $0x0,%esi
0x00000000004012b1 <+91>: mov $0x4080e0,%edi
0x00000000004012b6 <+96>: callq 0x401954 <std::__atomic_base<int>::operator++(int)>
0x00000000004012bb <+101>: mov -0x8(%rbp),%rax
0x00000000004012bf <+105>: lea 0x1(%rax),%rcx
0x00000000004012c3 <+109>: movabs $0xabcc77118461cefd,%rdx
0x00000000004012cd <+119>: mov %rcx,%rax
0x00000000004012d0 <+122>: mul %rdx
0x00000000004012d3 <+125>: mov %rdx,%rax
0x00000000004012d6 <+128>: shr $0x19,%rax
0x00000000004012da <+132>: imul $0x2faf080,%rax,%rax
0x00000000004012e1 <+139>: sub %rax,%rcx
0x00000000004012e4 <+142>: mov %rcx,%rax
0x00000000004012e7 <+145>: test %rax,%rax
0x00000000004012ea <+148>: jne 0x401302 <TearTest()+172>
0x00000000004012ec <+150>: mov -0x10(%rbp),%rax
0x00000000004012f0 <+154>: mov %rax,%rsi
0x00000000004012f3 <+157>: mov $0x40402a,%edi
0x00000000004012f8 <+162>: mov $0x0,%eax
0x00000000004012fd <+167>: callq 0x401040 <printf@plt>
0x0000000000401302 <+172>: addq $0x1,-0x8(%rbp)
0x0000000000401307 <+177>: jmpq 0x401266 <TearTest()+16>
0x000000000040130c <+182>: mov -0x8(%rbp),%rax
0x0000000000401310 <+186>: mov %rax,%rsi
0x0000000000401313 <+189>: mov $0x4080e8,%edi
0x0000000000401318 <+194>: callq 0x401984 <std::__atomic_base<unsigned long>::operator+=(unsigned long)>
0x000000000040131d <+199>: nop
0x000000000040131e <+200>: leaveq
0x000000000040131f <+201>: retq
volatile int x;
int y;
volatile int z;
x = 1;
y = 2;
z = 3;
z = 4;
...
int a = x;
int b = x;
int c = y;
int d = z;