C++11 C++;11关于现代英特尔:我疯了还是非原子对齐的64位加载/存储实际上是原子的?

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

我是否可以将一个任务关键型应用程序建立在这个测试结果的基础上,即100个线程读取一个主线程10亿次设置的指针时,永远不会看到一个撕裂

除了撕裂,还有其他潜在的问题吗

下面是一个独立的演示,它使用
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;