web-dev-qa-db-ja.com

なぜコンパイラは冗長なstd :: atomic書き込みをマージしないのですか?

同じ値の連続した書き込みを単一のアトミック変数にマージするようにコンパイラーが準備されていない理由を疑問に思っています、例えば:

#include <atomic>
std::atomic<int> y(0);
void f() {
  auto order = std::memory_order_relaxed;
  y.store(1, order);
  y.store(1, order);
  y.store(1, order);
}

私が試したすべてのコンパイラは、上記の書き込みを3回発行します。上記のコードと、1回の書き込みで最適化されたバージョンの違いを確認できる正当な競合のないオブザーバー(つまり、「as-if」ルールは適用されません)

変数が揮発性だった場合、明らかに最適化は適用されません。私の場合、それを妨げているのは何ですか?

compiler Explorer のコードは次のとおりです。

47
PeteC

C++ 11/C++ 14標準文書のとおりは、3つのストアを最終値の1つのストアに折りたたむ/結合することを許可します。このような場合でも:

_  y.store(1, order);
  y.store(2, order);
  y.store(3, order); // inlining + constant-folding could produce this in real code
_

標準ではnotは、y(アトミックロードまたはCASを使用)でスピンするオブザーバが_y == 2_を参照することを保証します。これに依存するプログラムにはデータ競合のバグがありますが、C++未定義の動作のデータ競合ではなく、園芸品種の競合だけがあります。 (それは、非原子変数を持つUBのみです)。 sometimesを期待しているプログラムは、必ずしもバグがあるとは限りません。 (以下の「進行状況バー」を参照してください。)

C++抽象マシンで可能な順序は、(コンパイル時に)always発生する順序として選択できます。これは、実行中のas-ifルールです。この場合、それはas ifです。3つのストアすべてがグローバルな順序で連続して発生し、_y=1_と_y=3_の間で他のスレッドからのロードまたはストアは発生しません。

ターゲットのアーキテクチャやハードウェアに依存しません。厳密に順序付けされたx86をターゲットとする場合でも、リラックスしたアトミック操作の コンパイル時の並べ替え が許可されます。コンパイラーは、コンパイルするハードウェアについて考えることから期待するものを保存する必要がないため、障壁が必要です。バリアはゼロasm命令にコンパイルされる場合があります。


では、なぜコンパイラはこの最適化を行わないのでしょうか?

これは実装品質の問題であり、実際のハードウェアで観察されるパフォーマンス/動作を変更する可能性があります。

問題である最も明らかなケースは、プログレスバーです。ストアをループ(他のアトミック操作を含まない)からシンクし、それらをすべて1つにフォールドすると、プログレスバーが0のままで、最後に100%になります。

必要ない場合にstopを実行するC++ 11 _std::atomic_方法はないため、現在のコンパイラは、複数のアトミック操作を1つに結合しないことを選択します。 (すべてを1つの操作に結合しても、互いの順序は変わりません。)

コンパイラライターは、ソースがy.store()を実行するたびに、アトミックストアが実際にメモリに発生することをプログラマが期待していることを正しく認識しています。 (この質問に対する他の回答のほとんどを参照してください。中間値を見るのを待っている可能性のある読者のために、ストアは個別に発生する必要があると主張しています。)つまり、 最小限の驚きの原則に違反しています

ただし、たとえば、ループ内の無用な_shared_ptr_ ref count inc/decを回避するなど、非常に役立つ場合があります。

明らかに、並べ替えや結合は、他の順序付け規則に違反することはできません。たとえば、numのメモリに触れなくなった場合でも、_num++; num--;_は実行時およびコンパイル時の並べ替えに対する完全なバリアである必要があります。


ディスカッションは_std::atomic_ APIを拡張するために進行中であり、プログラマーはそのような最適化を制御できます。 、意図的に非効率的ではない慎重に記述されたコードでも発生する可能性があります。最適化の有用な事例のいくつかの例は、次のワーキンググループのディスカッション/提案リンクで言及されています。

リチャード・ホッジスの num ++は 'int num'に対してアトミックにできますか? (コメントを参照)についても、同じトピックに関する説明を参照してください。同じ質問に対する my answer の最後のセクションも参照してください。ここでは、この最適化が許可されていることをさらに詳しく説明します。 (C++ワーキンググループのリンクはすでに書かれている現在の標準では許可されており、現在のコンパイラは意図的に最適化されていないことを既に認識しているため、ここでは省略します。)


現在の標準では、_volatile atomic<int> y_は、そこへのストアが最適化されないようにする方法の1つです。 ( Herb SutterがSO answer で指摘しているように、volatileatomicはすでにいくつかの要件を共有していますが、 _std::memory_order_とcppreferenceのvolatile との関係も参照してください。

volatileオブジェクトへのアクセスは、最適化されては許可されません(たとえば、メモリマップIOレジスタ)である可能性があるため)。

_volatile atomic<T>_を使用すると、ほとんど進行状況バーの問題が修正されますが、C++が最適化を制御するためのさまざまな構文を決定し、コンパイラーが実際にそれを実行できるようになると、数年後には愚かに見えるかもしれません。

コンパイラは、この最適化を制御する方法ができるまで、この最適化を開始しないと確信できると思います。 C++ whateverとしてコンパイルされた場合、既存のコードC++ 11/14コードの動作を変更しない何らかのオプトイン(_memory_order_release_coalesce_など)になることを願っています。しかし、wg21/p0062の提案のように、タグは_[[brittle_atomic]]_でケースを最適化しないでください。

wg21/p0062は、_volatile atomic_でさえすべてを解決できないと警告し、この目的での使用を推奨しません。この例を示します。

_if(x) {
    foo();
    y.store(0);
} else {
    bar();
    y.store(0);  // release a lock before a long-running loop
    for() {...} // loop contains no atomics or volatiles
}
// A compiler can merge the stores into a y.store(0) here.
_

_volatile atomic<int> y_を使用しても、コンパイラーは_if/else_からy.store()をシンクして1回だけ実行できます。これは、同じ値で正確に1つのストアを実行しているためです。 (elseブランチの長いループの後にあります)。特に、ストアが_seq_cst_ではなくrelaxedまたはreleaseのみの場合。

volatileは質問で説明した合体を停止しますが、これは_atomic<>_の他の最適化も実際のパフォーマンスにとって問題になる可能性があることを指摘しています。


最適化を行わない他の理由には、コンパイラーがこれらの最適化を安全に(間違いを起こさずに)行える複雑なコードを誰も書いていません。 N4455は、LLVMがすでに述べた最適化のいくつかを既に実装しているか、簡単に実装できると言うので、これは十分ではありません。

しかし、プログラマにとって混乱の理由は確かにもっともらしいです。ロックフリーコードは、そもそも正しく記述するのに十分困難です。

原子兵器の使用に無頓着にならないでください。それらは安価ではなく、あまり最適化されていません(現在はまったくありません)。 _std::shared_ptr<T>_を使用して冗長なアトミック操作を回避するのは必ずしも簡単ではありませんが、非原子バージョンはないため( ここでの答えの1つ は、 gccの_shared_ptr_unsynchronized<T>_を定義します)。

36
Peter Cordes

あなたは、デッドストアの除去について言及しています。

アトミックデッドストアを削除することは禁止されていませんが、アトミックストアがそのように適格であることを証明することは困難です。

デッドストアの削除などの従来のコンパイラの最適化は、アトミック操作で実行できます。
オプティマイザーは、synchronizationポイント間でそうしないように注意する必要があります。実行の別のスレッドがメモリを監視または変更できるためです。つまり、従来の最適化では、アトミック操作の最適化を検討する際に通常よりも介在する命令。
デッドストアの削除の場合、アトミックストアが他のストアを削除するために他のストアが他を支配しエイリアスすることを証明するだけでは不十分です。

from N4455健全なコンパイラはアトミックを最適化しません

アトミックDSEの問題は、一般的な場合、同期ポイントの検索を伴うことです。私の理解では、この用語は、コードの中でポイントhappen-before関係があることを意味しますスレッドAの命令とanotherスレッドBの命令.

スレッドAによって実行されるこのコードを検討してください。

_y.store(1, std::memory_order_seq_cst);
y.store(2, std::memory_order_seq_cst);
y.store(3, std::memory_order_seq_cst);
_

y.store(3, std::memory_order_seq_cst)として最適化できますか?

スレッドBが_y = 2_の表示を待機している場合(たとえば、CASを使用している場合)、コードが最適化された場合、それは観察されません。

ただし、私の理解では、_y = 2_でBループとCASsingを行うことは、2つのスレッドの命令間に完全な順序がないため、データの競合です。
Bのループが観察可能になる前にAの命令が実行される(つまり許可される)ため、コンパイラはy.store(3, std::memory_order_seq_cst)に最適化できます。

スレッドAとBがスレッドAのストア間で何らかの形で同期されている場合、最適化は許可されません(半順序が誘導され、Bが_y = 2_を潜在的に監視することになります)。

このような同期がないことを証明することは、より広い範囲を検討し、アーキテクチャのすべての癖を考慮することを伴うため困難です。

私の理解に関しては、原子操作の比較的小さい年齢とメモリの順序付け、可視性および同期に関する推論の困難さのために、コンパイラは、必要なものを検出して理解するためのより堅牢なフレームワークまで原子上の可能な最適化をすべて実行しません条件が構築されます。

あなたの例は、他のスレッドや同期ポイントを持たないため、上記のカウントスレッドの単純化であると思います。私が見ることができるのは、コンパイラが3つのストアを最適化できたからです。

41
Margaret Bloom

1つのスレッドでアトミックの値を変更している間、他のスレッドがそれをチェックし、アトミックの値に基づいて操作を実行している場合があります。あなたが与えた例は非常に具体的であるため、コンパイラ開発者は最適化する価値がありません。ただし、1つのスレッドが設定されている場合アトミックの連続値:012など、他のスレッドがアトミックの値によって示されるスロットに何かを入れている可能性があります。

9
Serge Rogatch

要するに、標準(たとえば、[intro.multithread]の20前後のパラガラフ)が許可していないためです。

満たされなければならない事前保証があり、とりわけ、並べ替えまたは結合の書き込みを除外します(19項は並べ替えについても明示的に述べています)。

スレッドが次々に3つの値(1、2、3など)をメモリに書き込む場合、別のスレッドが値を読み取る可能性があります。たとえば、スレッドが中断された場合(または並行して実行された場合でも)、別のスレッドalsoがその場所に書き込みを行う場合、監視スレッドは発生した順序とまったく同じ順序で操作を確認する必要があります(スケジューリングまたは偶然のいずれか、または何らかの理由で)。それは保証です。

書き込みの半分(または1つだけ)を行う場合、これはどのように可能ですか?そうではありません。

スレッドが代わりに1 -1 -1を書き出し、別のスレッドが散発的に2または3を書き出した場合はどうなりますか? 3番目のスレッドが場所を監視し、最適化されたために表示されない特定の値を待機するとどうなりますか?

ストア(およびロードも)が要求どおりに実行されない場合に提供される保証を提供することは不可能です。それらはすべて、同じ順序で。

5
Damon

NB:私はこれをコメントするつもりでしたが、それは少し冗長です。

興味深い事実の1つは、この動作はC++のデータ競合ではないということです。

P.14の注21は興味深い: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3690.pdf (私の強調):

プログラムの実行には、少なくともの2つの競合するアクションが含まれている場合、データ競合が含まれます。そのうちの1つはアトミックではありません

P.11注5でも:

「緩和された」アトミック操作は、同期操作のようにデータの競合に寄与できない場合でも、同期操作ではありません。

そのため、アトミックでの競合するアクションは、C++標準の観点では決してデータ競合ではありません。

これらの操作はすべてアトミック(および特にリラックス)ですが、ここではデータ競合はありません!

どの(合理的な)プラットフォームでも、これら2つの間に信頼性/予測可能な違いがないことに同意します。

_include <atomic>
std::atomic<int> y(0);
void f() {
  auto order = std::memory_order_relaxed;
  y.store(1, order);
  y.store(1, order);
  y.store(1, order);
}
_

そして

_include <atomic>
std::atomic<int> y(0);
void f() {
  auto order = std::memory_order_relaxed;
  y.store(1, order);
}
_

しかし、C++メモリモデルが提供する定義では、データの競合ではありません。

その定義が提供されている理由を簡単に理解することはできませんが、開発者にいくつかのカードを渡して、(プラットフォーム上で)統計的に機能することがわかっているスレッド間で無秩序な通信を行います。

たとえば、値を3回設定してから読み戻すと、その場所に対するある程度の競合が表示されます。そのようなアプローチは決定論的ではありませんが、多くの効果的な並行アルゴリズムは決定論的ではありません。たとえば、タイムアウトしたtry_lock_until()は常に競合状態ですが、有用なテクニックのままです。

C++標準は、「データの競合」に関する確実性を提供しますが、最終分析のさまざまな状況にある競合状態の特定の楽しみとゲームを許可します。

一言で言えば、標準は、他のスレッドが値を3回設定する「ハンマー」効果を見る可能性がある場合、他のスレッドはその効果を見ることができなければならないことを規定しているようです(たとえそうでない場合でも!)状況によっては他のスレッドがハンマーを使用する可能性のある最新のプラットフォームのほとんどすべてが該当します。

5
Persixty

スレッドがyに依存または変更しない更新間で重要なことを行う場合、パターンの実用的な使用例は次のようになります。*スレッド2はyの値を読み取って、スレッド1の進捗状況。

したがって、おそらくスレッド1はステップ1として構成ファイルをロードし、その解析された内容をステップ2としてデータ構造に入れ、メインウィンドウをステップ3として表示し、スレッド2はステップ2が完了するのを待って、データ構造に依存する別のタスクを並行して実行します。 (許可、この例では、リラックスした順序ではなく、取得/解放セマンティクスが必要です。)

準拠する実装により、スレッド1が中間ステップでyを更新しないようにすることができると確信しています。言語標準について詳しく調べていませんが、別のスレッドのハードウェアをサポートしていない場合はショックを受けますポーリングyは、値2を表示しない場合があります。

ただし、これは、ステータスの更新を最適化することは悲観的である可能性がある仮想インスタンスです。おそらくコンパイラ開発者がここに来て、そのコンパイラが選択しない理由を言うかもしれませんが、考えられる理由の1つは、足を踏み入れるか、少なくともつま先を突き刺すことです。

2
Davislor

3つの店舗がすぐ隣にあるという病的なケースから少し離れてみましょう。店舗間で行われている重要な作業がいくつかあり、そのような作業にはyがまったく関与していないと仮定します(データパス分析では、少なくともこの中で、3つの店舗が実際に冗長であると判断できますスレッド)、およびそれ自体はメモリバリアを導入しません(そのため、他の何かがストアを他のスレッドから見えないようにします)。これで、他のスレッドがストア間で作業を行う機会があり、おそらく他のスレッドがyを操作し、このスレッドが1(2番目のストア)にリセットする必要がある理由がある可能性があります。最初の2つのストアが削除された場合、動作が変更されます。

0
Andre Kostur