web-dev-qa-db-ja.com

std :: atomicは揮発性である必要がありますか?

フラグが設定されるまで実行されるスレッドを実行しています。

_std::atomic<bool> stop(false);

void f() {
  while(!stop.load(std::memory_order_{relaxed,acquire})) {
    do_the_job();
  }
}
_

コンパイラがこのようにループを展開できるかどうか疑問に思います(私はそれが起こらないようにしたいです)。

_void f() {
  while(!stop.load(std::memory_order_{relaxed,acquire})) {
    do_the_job();
    do_the_job();
    do_the_job();
    do_the_job();
    ... // unroll as many as the compiler wants
  }
}
_

揮発性と原子性は直交していると言われていますが、少し混乱しています。コンパイラはアトミック変数の値を自由にキャッシュしてループを展開できますか?コンパイラがループを展開できる場合は、フラグにvolatileを付ける必要があると思いますが、確実にしたいと思います。

volatileを入れるべきですか?


あいまいになってすみません。私は(私が推測しているように)並べ替えとは何か、_memory_order_*_ sの意味を理解しており、volatileとは何かを完全に理解していると確信しています。

while()ループは、このような無限のifステートメントとして変換できると思います。

_void f() {
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  do_the_job();
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  do_the_job();
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  do_the_job();
  ...
}
_

与えられたメモリオーダリングは、シーケンス前の操作がアトミックロードを超えて移動することを妨げないので、揮発性がなければ再配置できると思います。

_void f() {
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  ...
  do_the_job();
  do_the_job();
  do_the_job();
  ...
}
_

アトミックが揮発性を意味しない場合、最悪の場合、コードはこのように変換することさえできると思います。

_void f() {
  if(stop.load(std::memory_order_{relaxed,acquire})) return;

  while(true) {
    do_the_job();
  }
}
_

このような非常識な実装は決してありませんが、それでも可能性のある状況だと思います。これを防ぐ唯一の方法は、アトミック変数にvolatileを入れて、それについて質問することだと思います。

私が推測したことはたくさんありますが、何か問題があれば教えてください。

19
Inbae Jeong

コンパイラはアトミック変数の値を自由にキャッシュしてループを展開できますか?

コンパイラーは、アトミック変数の値をキャッシュできません。

ただし、std::memory_order_relaxedを使用しているため、コンパイラは、他のロードおよびストアに関して、このアトミック変数との間でロードおよびストアを自由に並べ替えることができます。

また、この変換ユニットで定義が使用できない関数の呼び出しは、コンパイラのメモリバリアであることに注意してください。つまり、周囲のロードとストアに関して呼び出しを並べ替えることはできず、すべての非ローカル変数は、すべて揮発性とマークされているかのように、呼び出し後にメモリから再ロードする必要があります。 (ただし、アドレスが他の場所に渡されなかったローカル変数は再ロードされません)。

回避したいコードの変換は、C++メモリモデルに違反するため、有効な変換ではありません。最初のケースでは、アトミック変数を1回ロードした後、 do_the_jobを呼び出します。2番目に、複数の呼び出しがあります。変換されたコードの観察された動作は異なる場合があります。


そして std :: memory_order からのメモ:

volatileとの関係

実行スレッド内では、すべての揮発性オブジェクトへのアクセス(読み取りと書き込み)が相互に並べ替えられないことが保証されますが、揮発性アクセスはスレッド間の同期を確立しないため、この順序が別のスレッドによって監視されることは保証されません。 。

さらに、揮発性アクセスはアトミックではなく(同時読み取りと書き込みはデータ競合です)、メモリを順序付けません(不揮発性メモリアクセスは、揮発性アクセスの周囲で自由に並べ替えることができます) 。

このビットの不揮発性メモリアクセスは、揮発性アクセスの周囲で自由に並べ替えることができますは、リラックスしたアトミックにも当てはまります。リラックスしたロードとストアは、他のロードとストアに関して並べ替えることができるためです。 。

つまり、アトミックをvolatileで装飾しても、コードの動作は変わりません。


とにかく、C++ 11アトミック変数はvolatileキーワードでマークする必要はありません。


これは、g ++-5.2がアトミック変数を尊重する方法の例です。次の機能:

__attribute__((noinline)) int f(std::atomic<int>& a) {
    return a.load(std::memory_order_relaxed);
}

__attribute__((noinline)) int g(std::atomic<int>& a) {
    static_cast<void>(a.load(std::memory_order_relaxed));
    static_cast<void>(a.load(std::memory_order_relaxed));
    static_cast<void>(a.load(std::memory_order_relaxed));
    return a.load(std::memory_order_relaxed);
}

__attribute__((noinline)) int h(std::atomic<int>& a) {
    while(a.load(std::memory_order_relaxed))
        ;
    return 0;
}

g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.Sでコンパイルすると、次のアセンブリが生成されます。

f(std::atomic<int>&):
    movl    (%rdi), %eax
    ret

g(std::atomic<int>&):
    movl    (%rdi), %eax
    movl    (%rdi), %eax
    movl    (%rdi), %eax
    movl    (%rdi), %eax
    ret

h(std::atomic<int>&):
.L4:
    movl    (%rdi), %eax
    testl   %eax, %eax
    jne .L4
    ret
8