C++ 11標準では、マシンモデルがシングルスレッドマシンからマルチスレッドマシンに変更されました。
これは、最適化された読み取りの典型的なstatic int x; void func() { x = 0; while (x == 0) {} }
の例が、C++ 11では発生しなくなることを意味しますか?
編集:この例を知らない人(私は真剣に驚いています)のために、これを読んでください: https://en.wikipedia.org/wiki/Volatile_variable
EDIT2:OK、私はvolatile
が何であるかを知っているすべての人がこの例を見たことを本当に期待していました。
例のコードを使用すると、サイクルで読み取られた変数が最適化され、サイクルが無限になります。
もちろん、解決策はvolatile
を使用することです。これにより、コンパイラはアクセスごとに変数を読み取るように強制されます。
私の質問は、マシンモデルがマルチスレッドであるため、これがC++ 11で非推奨の問題であるかどうかです。したがって、コンパイラは変数への同時アクセスがシステムに存在することを考慮する必要があります。
それが最適化されるかどうかは、コンパイラーとコンパイラーが何を最適化するかによって完全に異なります。 C++ 98/03メモリモデルは、x
がその設定と値の取得の間で変更される可能性を認識していません。
C++ 11メモリモデルはx
が変更される可能性があることを認識します。ただし、気にしません。変数への非アトミックアクセス(つまり、std::atomic
sまたは適切なミューテックスを使用しない)は、未定義の動作をもたらします。したがって、C++ 11コンパイラでは、書き込みと読み取りの間でx
が変更されないと想定することはまったく問題ありません。未定義の動作は、「関数はx
の変更を決して認識しない」ことを意味する可能性があるためです。
それでは、C++ 11がvolatile int x;
について何と言っているかを見てみましょう。そこにそれを入れて、x
で他のスレッドが混乱している場合、まだ未定義の動作があります。揮発性はスレッドの動作には影響しません。 C++ 11のメモリモデルは、x
との間の読み取りまたは書き込みをアトミックに定義していません。また、非アトミック読み取り/書き込みに必要なメモリバリアを適切に順序付ける必要もありません。 volatile
は、それとは何の関係もありません。
ああ、あなたのコードはうまくいくかもしれません。しかし、C++ 11はそれを保証しません。
volatile
がコンパイラに伝えることは、その変数からのメモリ読み取りを最適化できないということです。ただし、CPUコアには異なるキャッシュがあり、ほとんどのメモリ書き込みはすぐにメインメモリに出力されません。それらはそのコアのローカルキャッシュに保存され、書き込まれる可能性があります...最終的に。
CPUには、キャッシュラインをメモリに強制的に出力し、異なるコア間でメモリアクセスを同期する方法があります。これらのメモリバリアにより、2つのスレッドが効果的に通信できます。別のコアで書き込まれた1つのコアのメモリから読み取るだけでは不十分です。メモリを書き込んだコアはバリアを発行する必要があり、メモリを読み取っているコアは、実際にデータを取得するために、メモリを読み取る前にそのバリアを完了している必要があります。
volatile
はこれのどれもを保証しません。 Volatileは、「ハードウェア、マップされたメモリなど」で動作します。これは、そのメモリを書き込むハードウェアが、キャッシュの問題を確実に処理するためです。 CPUコアが書き込みのたびにメモリバリアを発行した場合、基本的にパフォーマンスの希望に別れを告げることができます。そのため、C++ 11には、バリアを発行するために構成が必要な場合を示す特定の言語があります。
volatile
はメモリaccess(いつ読み取るか)に関するものです。スレッド化はメモリ整合性(実際にそこに格納されているもの)に関するものです。
C++ 11メモリモデルは、あるスレッドでの書き込みが別のスレッドで表示されるようにする操作について固有です。それはメモリの整合性についてであり、volatile
が処理するものではありません。また、メモリの整合性には通常、両方のスレッドが何かを行う必要があります。
たとえば、スレッドAがミューテックスをロックし、書き込みを行ってからロックを解除した場合、C++ 11メモリモデルでは、スレッドBが後でロックした場合にのみ、書き込みがスレッドBに表示されるようにする必要があります。実際にその特定のロックを取得するまで、そこにある値は未定義です。このようなものは、規格のセクション1.10に詳細に説明されています。
標準に関して あなたが引用するコード を見てみましょう。セクション1.10、p8は、特定のライブラリ呼び出しがスレッドを別のスレッドと「同期」させる機能について説明しています。他の段落のほとんどは、同期(およびその他のもの)がスレッド間の操作の順序を構築する方法を説明しています。もちろん、あなたのコードはこれのどれも呼び出しません。同期ポイント、依存関係の順序、何もありません。
このような保護がなければ、何らかの形の同期や順序付けがなければ、1.10p21は次のようになります。
プログラムの実行には、データ競合が含まれます。これには、異なるスレッドで2つの競合するアクションが含まれ、そのうちの少なくとも1つはアトミックではなく、以前は発生しませんでした。もう1つ。このようなデータの競合は、未定義の動作を引き起こします。
プログラムには、2つの競合するアクション(x
からの読み取りとx
への書き込み)が含まれています。どちらもアトミックではなく、同期によって他の前に発生するように順序付けられていません。
したがって、未定義の動作を実現しました。
したがって、C++ 11メモリモデルによって保証されたマルチスレッド動作が得られる唯一のケースは、適切なミューテックスまたはstd::atomic<int> x
を使用する場合です。適切なアトミックロード/ストア呼び出し。
ああ、あなたもx
を揮発性にする必要はありません。 (非インライン)関数を呼び出すときはいつでも、その関数またはそれが呼び出す何かがグローバル変数を変更する可能性があります。したがって、cannotx
ループ内のwhile
の読み取りを最適化できません。また、同期するすべてのC++ 11メカニズムでは、関数を呼び出す必要があります。それはまさにメモリバリアを引き起こすために起こります。
Intel開発者ゾーンの言及 "揮発性:マルチスレッドプログラミングにはほとんど役に立たない"
Volatileキーワードは、このシグナルハンドラの例で使用されています cppreference.com
#include <csignal>
#include <iostream>
namespace
{
volatile std::sig_atomic_t gSignalStatus;
}
void signal_handler(int signal)
{
gSignalStatus = signal;
}
int main()
{
// Install a signal handler
std::signal(SIGINT, signal_handler);
std::cout << "SignalValue: " << gSignalStatus << '\n';
std::cout << "Sending signal " << SIGINT << '\n';
std::raise(SIGINT);
std::cout << "SignalValue: " << gSignalStatus << '\n';
}