1. ホーム
  2. c++

[解決済み] C++11では、標準化されたメモリモデルが導入されました。その意味するところは?そして、C++プログラミングにどのような影響を与えるのでしょうか?

2022-03-19 17:21:40

質問

C++11では、標準化されたメモリモデルが導入されましたが、これは具体的にどのような意味ですか?また、C++のプログラミングにどのような影響を与えるのでしょうか?

この記事 (by ギャビン・クラーク 引用者 ハーブ・サッター )はこう言っている。

メモリモデルとは、C++のコード を呼び出すための標準化されたライブラリを持つようになりました。 誰がコンパイラを作ったかに関係なく どのようなプラットフォームで動作しているのか。 どのように制御するかという標準的な方法があります。 異なるスレッドが プロセッサのメモリ

分割について話しているとき [異なるコアにまたがるコード 規格の中で、私たちが話しているのは メモリモデルです。私たちは を壊さずに最適化します。 次のような前提があります。 をコードで作ること,"。 サター は言った。

まあ、私は 暗記 というような、ネット上で公開されているようなパラグラフは、(生まれたときから自分の記憶モデルを持っているので:P)他の人から質問されたときに答えとして投稿することもできますが、正直なところ、これを正確に理解できているわけではありません。

C++プログラマは以前からマルチスレッドアプリケーションを開発していたわけで、POSIXスレッドだろうが、Windowsスレッドだろうが、C++11スレッドだろうが、関係ないのでは?どんなメリットがあるのでしょうか?低レベルの詳細を理解したい。

また、C++11のメモリモデルは、C++11のマルチスレッド対応と何らかの関係があるような気がします。この2つが一緒になっているのをよく見かけるからです。もしそうだとしたら、具体的にはどのように?また、なぜ関連性があるのでしょうか?

マルチスレッドの内部がどうなっているのか、一般的にメモリーモデルとはどういうものなのか、よく分からないので、これらの概念を理解するためにご協力をお願いします :-)

どのように解決するのですか?

まず、Language Lawyerのような考え方を身につける必要があります。

C++の仕様は、特定のコンパイラ、オペレーティングシステム、CPUに言及するものではありません。 参照するのは 抽象的なマシン 実際のシステムを一般化したものである。 言語弁護士の世界では、プログラマーの仕事は抽象的なマシンのためのコードを書くことであり、コンパイラの仕事はそのコードを具体的なマシンで実現することです。 仕様に忠実にコーディングすれば、現在でも50年後でも、C++コンパイラに準拠したシステムであれば、コードを修正することなくコンパイルして実行できることが保証されるのです。

C++98/C++03仕様の抽象マシンは、基本的にシングルスレッドである。 そのため、仕様に対して完全に移植可能なマルチスレッド C++ コードを書くことは不可能です。 この仕様では 原子性 メモリのロードとストアの オーダー のような、ロードとストアが発生する可能性のあるものは、ミューテックスのようなものは気にしないでください。

もちろん、pthreadsやWindowsのような特定の具象システムに対しては、実際にマルチスレッドコードを書くことができます。 しかし 標準 C++98/C++03でマルチスレッドコードを記述する方法。

C++11の抽象マシンは、設計上、マルチスレッドになっています。 また メモリモデル つまり、コンパイラがメモリにアクセスするときに、何をしてもよくて、何をしてはいけないかが書かれています。

次のような例で、2つのスレッドが同時に1組のグローバル変数にアクセスする場合を考えてみましょう。

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

スレッド2は何を出力するのでしょうか?

C++98/C++03では、これは未定義の動作ですらなく、質問そのものが 意味がない なぜなら、標準では、スレッドと呼ばれるものは想定されていないからです。

C++11では、ロードとストアは一般にアトミックである必要はないため、結果は未定義の動作となります。 これはあまり改善されていないように見えるかもしれませんが...。 そして、それ自体、そうではありません。

でも、C++11だとこう書けるんです。

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

さて、事態はもっと面白くなる。 まず第一に、ここでの動作は 定義済み . スレッド 2 は、次のように表示できます。 0 0 (スレッド1より先に実行されている場合)。 37 17 (スレッド1の後に実行される場合)、または 0 17 (スレッド 1 が x に割り当てた後、y に割り当てる前に実行される場合)。

プリントできないのは 37 0 C++11 では、アトミック ロード/ストアのデフォルト モードとして 順次的整合性 . これは、すべてのロードとストアが、各スレッド内では書いた順番通りに行われなければならないが、スレッド間の操作はシステムが好きなようにインターリーブできることを意味します。 つまり、アトミクスのデフォルトの動作では 原子性 順序付け ロードとストアのための

さて、最近のCPUでは、シーケンシャルの一貫性を確保するのは高価になりがちです。 特に、コンパイラはここにアクセスするたびに本格的なメモリバリアを出す可能性があります。 しかし、もしあなたのアルゴリズムが順不同のロードやストアを許容できるなら、つまり、原子性は必要だが順序は必要ないのなら、つまり、もしあなたのアルゴリズムが 37 0 をこのプログラムの出力として書くことができます。

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

最新のCPUであればあるほど、前の例より速くなる可能性が高いです。

最後に、特定のロードとストアを整理しておくだけなら、書くことができます。

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

これは、順序付けられたロードとストアに戻ります。 37 0 はもはや可能な出力ではありませんが、最小限のオーバーヘッドでこれを実現します。 (このつまらない例では、結果は本格的な逐次一貫性と同じですが、より大きなプログラムでは、そうではないでしょう)。

もちろん、もしあなたが見たい出力が 0 0 または 37 17 のように、元のコードにmutexを巻きつければいいのです。 しかし、もしあなたがここまで読んできたのなら、それがどのように機能するかはすでにご存知でしょうし、この回答は私が意図したよりもすでに長くなっています :-)。

というわけで、結論。ミュートスは素晴らしいもので、C++11では標準化されています。しかし、時にはパフォーマンス上の理由から、より低レベルのプリミティブ(例えば、古典的な ダブルチェックロックパターン ). 新しい規格では、ミューテックスや条件変数といった高レベルのガジェットと、アトミック型や様々な種類のメモリバリアといった低レベルのガジェットが提供されます。 このため、洗練された高性能な並行処理ルーチンを、すべて標準規格で指定された言語で書くことができ、そのコードは現在のシステムでも将来のシステムでも変わらずにコンパイル・実行されることが保証されている。

でも、正直なところ、よほど専門家で低レベルのコードに取り組んでいる人でなければ、ミューテックスと条件変数にこだわるべきでしょう。 私はそうするつもりだ。

この内容については このブログの記事 .