24

そこで私は、来たる C++0x 標準の一部であるメモリ モデルについて読んでいました。ただし、コンパイラーが許可されていること、特に投機的なロードとストアに関するいくつかの制限については、少し混乱しています。

まず、関連するもののいくつか:

C++0x のスレッドとメモリ モデルに関する Hans Boehm のページ

Boehm、「スレッドはライブラリとして実装できない」

Boehm と Adve、「C++ 同時実行メモリ モデルの基礎」

Sutter、「Prism: Microsoft ネイティブ コード プラットフォーム向けの原理ベースのシーケンシャル メモリ モデル」、N2197

Boehm、「同時実行メモリ モデル コンパイラの結果」、N2338

現在、基本的な考え方は本質的に「データ競合のないプログラムの順次整合性」です。これは、プログラミングの容易さと、コンパイラとハードウェアの機会を最適化できるようにすることとの間の適切な妥協点のようです。データ競合は、異なるスレッドによる同じメモリ ロケーションへの 2 つのアクセスが順序付けられておらず、そのうちの少なくとも 1 つがメモリ ロケーションに格納され、そのうちの少なくとも 1 つが同期アクションではない場合に発生するように定義されています。これは、共有データへのすべての読み取り/書き込みアクセスが、ミューテックスやアトミック変数の操作など、何らかの同期メカニズムを介して行われる必要があることを意味します (まあ、専門家のみが緩和されたメモリ順序でアトミック変数を操作することは可能ですが、デフォルトではシーケンシャルな一貫性のため)。

これに照らして、通常の共有変数に対するスプリアスまたは投機的なロード/ストアに関する制限について混乱しています。たとえば、N2338 には次の例があります。

switch (y) {
    case 0: x = 17; w = 1; break;
    case 1: x = 17; w = 3; break;
    case 2: w = 9; break;
    case 3: x = 17; w = 1; break;
    case 4: x = 17; w = 3; break;
    case 5: x = 17; w = 9; break;
    default: x = 17; w = 42; break;
}

コンパイラが変換することを許可されていない

tmp = x; x = 17;
switch (y) {
    case 0: w = 1; break;
    case 1: w = 3; break;
    case 2: x = tmp; w = 9; break;
    case 3: w = 1; break;
    case 4: w = 3; break;
    case 5: w = 9; break;
    default: w = 42; break;
}

y == 2 の場合、x への偽の書き込みがあり、別のスレッドが x を同時に更新している場合に問題になる可能性があるためです。しかし、なぜこれが問題なのですか?これはデータ競合であり、とにかく禁止されています。この場合、コンパイラは x に 2 回書き込むことで状況を悪化させますが、1 回の書き込みでもデータ競合には十分ですよね? つまり、適切な C++0x プログラムは x へのアクセスを同期する必要があります。その場合、データ競合はなくなり、スプリアスストアも問題になりませんか?

N2197 の例 3.1.3 と他のいくつかの例についても同様に混乱していますが、上記の問題の説明でそれも説明できるかもしれません。

編集:答え:

投機的ストアが問題となる理由は、上記の switch ステートメントの例では、y != 2 の場合に限り x を保護するロックを条件付きで獲得することをプログラマーが選択した可能性があるためです。元のコードであるため、変換は禁止されています。同じ議論が N2197 の例 3.1.3 にも当てはまります。

4

2 に答える 2

8

私はあなたが参照しているすべてのものに精通しているわけではありませんが、y==2 の場合、コードの最初のビットでは、x がまったく書き込まれていない (または読み取られない) ことに注意してください。コードの 2 番目のビットでは、それが 2 回書かれています。これは、1 回の書き込みと 2 回の書き込みの違いよりも大きな違いです (少なくとも、pthread などの既存のスレッド モデルでは違います)。また、他の方法ではまったく保存されない値を保存することは、1 回保存する場合と 2 回保存する場合よりも大きな違いがあります。これらの両方の理由から、コンパイラが no-op を に置き換えるだけでは望ましくありませんtmp = x; x = 17; x = tmp;

スレッド A が、他のスレッドが x を変更しないと想定したいとします。y が 2 の場合、値を x に書き込んでから読み戻すと、書き込んだ値が返されることを期待できるようにするのが合理的です。しかし、スレッド B がコードの 2 番目のビットを同時に実行している場合、スレッド A は x に書き込み、後でそれを読み取り、元の値を取得できます。これは、スレッド B が書き込みの「前」に保存し、「後」に復元するためです。または、スレッド B が書き込みの「後」に 17 を格納し、スレッド A の読み取りの「後」に tmp を再度格納したため、17 が返される可能性があります。スレッド A は好きな同期を行うことができますが、スレッド B は同期されていないため、役に立ちません。同期されていない理由 (y==2 の場合) は、x を使用していないためです。

要するに、提案した変換が許可され、偽の書き込みが導入された場合、コードの一部を分析して、x (または他のメモリ位置) を変更しないと結論付けることは決してできません。同期せずにスレッド間で不変データを共有するなど、不可能な便利なイディオムがいくつかあります。

したがって、C++0x の「データ競合」の定義には詳しくありませんが、オブジェクトが書き込まれていないとプログラマーが想定できるいくつかの条件が含まれており、この変換がそれらの条件に違反することになると思います。y==2 の場合、元のコードと並行コード:x = 42; x = 1; z = x別のスレッドでは、データ競合として定義されていないと推測します。または、少なくともデータ競合である場合、z が 17 または 42 のいずれかの値になることを許可するものではありません。

このプログラムでは、y の値 2 を使用して、「実行中の他のスレッドがあります。x を変更しないでください。ここでは同期されていないため、データ競合が発生する可能性があります」ということを示しているとします。おそらく、同期がまったく行われない理由は、y の他のすべてのケースでは、x にアクセスして実行されている他のスレッドがないためです。C++0x が次のようなコードをサポートすることは、私には理にかなっているように思えます。

if (single_threaded) {
    x = 17;
} else {
    sendMessageThatSafelySetsXTo(17);
}

明らかに、それを次のように変換したくありません。

tmp = x;
x = 17;
if (!single_threaded) {
    x = tmp;
    sendMessageThatSafelySetsXTo(17);
}

これは基本的にあなたの例と同じ変換ですが、適切なコードサイズの最適化のように見せるのに十分ではなく、2 つのケースしかありません。

于 2010-01-04T20:31:04.203 に答える
5

y==2がであり、別のスレッドが を変更または読み取る場合x、元のサンプルに競合状態があるのはなぜですか? このスレッドは に触れないxので、他のスレッドは自由に触れることができます。

しかし、並べ替えられたバージョンでは、x一時的であってもスレッドが を変更するため、別のスレッドもそれを操作すると、以前は存在しなかった競合状態が発生します。

于 2010-01-04T20:43:28.610 に答える