web-dev-qa-db-ja.com

C++ 11では、標準化されたメモリモデルが導入されました。どういう意味ですか?そして、それはC++プログラミングにどのように影響するのでしょうか。

C++ 11では標準化されたメモリモデルが導入されましたが、それはどういう意味ですか?そして、それはC++プログラミングにどのように影響するのでしょうか。

この記事 (by Gavin Clarke 誰がHerb Sutterを引用と言う)

メモリモデルは、誰がコンパイラを作ったのか、そしてどのプラットフォームで動いているのかにかかわらず、C++コードが呼び出すべき標準化されたライブラリを持つことを意味します。さまざまなスレッドがプロセッサのメモリと通信する方法を制御する標準的な方法があります。

「標準の異なるコアにまたがって[コード]を分割することについて話しているとき、私たちはメモリモデルについて話しています。私たちは、人々がコードで作ることになる次の仮定を破ることなくそれを最適化します。」 サター 言った。

まあ、私はmemorizeとこれに似た段落をオンラインで見ることができます(私は生まれてから自分の記憶モデルを持っていたので:P)、他の人からの質問への答えとして投稿することさえできます。正確には理解できません。

C++プログラマは以前からマルチスレッドアプリケーションを開発していましたが、POSIXスレッド、Windowsスレッド、C++ 11スレッドのどれが問題になるでしょうか。利点は何ですか?低レベルの詳細を理解したいのですが。

私は、これら2つをよく見るように、C++ 11メモリモデルはC++ 11のマルチスレッドサポートに何らかの形で関係しているとも感じます。もしそうなら、どのように正確に?それらはどうして関連しているのでしょうか。

マルチスレッドの内部構造がどのように機能するのか、またメモリモデルが一般的にどのような意味を持つのかわからないので、これらの概念を理解するのを手伝ってください。 :-)

1724
Nawaz

まず、言語弁護士のように考えることを学ぶ必要があります。

C++仕様は、特定のコンパイラ、オペレーティングシステム、またはCPUを参照していません。 抽象的なマシンを参照します。これは実際のシステムの一般化です。言語弁護士の世界では、プログラマーの仕事は抽象マシンのコードを書くことです。コンパイラの仕事は、そのコードを具体的なマシンで実現することです。仕様に厳密にコーディングすることで、現在または50年後のいずれであっても、準拠C++コンパイラを備えたシステムでコードを変更せずにコンパイルおよび実行することができます。

C++ 98/C++ 03仕様の抽象マシンは、基本的にシングルスレッドです。そのため、仕様に関して「完全に移植可能」なマルチスレッドC++コードを記述することはできません。仕様では、メモリのロードとストアの原子性またはorderロードとストアが発生する可能性がありますが、ミューテックスなどは気にしないでください。

もちろん、pthreadやWindowsなどの特定の具体的なシステムに対して、実際にマルチスレッドコードを書くことができます。しかし、C++ 98/C++ 03用のマルチスレッドコードを記述するstandard方法はありません。

C++ 11の抽象マシンは、設計によりマルチスレッド化されています。また、明確に定義されたメモリモデル;つまり、メモリにアクセスする際にコンパイラが実行できることと実行できないことを示しています。

グローバル変数のペアが2つのスレッドによって同時にアクセスされる次の例を考えてみましょう。

           Global
           int x, y;

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

スレッド2は何を出力しますか?

C++ 98/C++ 03では、これは未定義の動作ではありません。質問自体は、meanlessです。これは、標準では「スレッド」と呼ばれるものを何も考慮していないためです。

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;

今、物事はもっと面白くなります。まず、ここでの動作はdefinedです。スレッド2は、0 0(スレッド1の前に実行される場合)、37 17(スレッド1の後に実行される場合)、または0 17(スレッド1がxに割り当てた後に実行される場合に、 y)に割り当てる前。

C++ 11のアトミックロード/ストアのデフォルトモードは、強制的にシーケンス整合性であるため、印刷できないのは37 0です。これは、すべてのロードとストアが各スレッド内で記述した順序で発生したかのように「すべて」行われる必要があることを意味します。一方、スレッド間の操作はシステムの好きな方法でインターリーブできます。したがって、アトミックのデフォルトの動作は、atomicityorderingの両方を提供します店舗。

現在、最新の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はそれらを標準化します。ただし、パフォーマンス上の理由から、低レベルのプリミティブが必要な場合があります(たとえば、古典的な 二重チェックロックパターン )。新しい標準は、ミューテックスや条件変数などの高レベルのガジェットを提供し、アトミックタイプやさまざまな種類のメモリバリアなどの低レベルのガジェットも提供します。したがって、標準で指定された言語で完全に洗練された高性能の並行ルーチンを作成できるようになり、現在のシステムと明日のシステムの両方でコードを変更せずにコンパイルして実行できます。

率直に言って、あなたが専門家であり、深刻な低レベルのコードに取り組んでいない限り、おそらくミューテックスと条件変数に固執すべきです。それが私がやろうとしていることです。

この機能の詳細については、 このブログ投稿 を参照してください。

2053
Nemo

メモリ整合性モデル(またはメモリモデル、略して)を理解するためのアナロジーを示します。それは、レスリーランポートの独創的な論文 「時間、クロック、および分散システムでのイベントの順序付け」 に触発されています。類推は適切であり、基本的な重要性を持っていますが、多くの人にとってはやり過ぎかもしれません。ただし、メモリの一貫性モデルについての推論を容易にする精神的なイメージ(画像表現)を提供することを望みます。

すべてのメモリ位置の履歴を時空間図で表示してみましょう。横軸はアドレス空間を表し(つまり、各メモリ位置はその軸上の点で表されます)、縦軸は時間を表します(これは、一般的に、時間の普遍的な概念はありません)。したがって、各メモリ位置に保持されている値の履歴は、そのメモリアドレスの垂直列で表されます。値が変更されるたびに、スレッドの1つがその場所に新しい値を書き込みます。 メモリイメージによって、観測可能なすべてのメモリ位置の値の集合/組み合わせを意味します特定の時間 by 特定のスレッド

「メモリの一貫性とキャッシュの一貫性に関する入門書」からの引用

直感的(かつ最も制限の厳しい)メモリモデルは、スレッドがシングルコアプロセッサ上で時間多重化されているかのように、マルチスレッド実行が各構成スレッドの順次実行のインターリーブのように見える順次整合性(SC)です。

そのグローバルメモリの順序は、プログラムの実行ごとに異なる可能性があり、事前に知ることはできません。 SCの特徴は、同時性の平面(つまり、メモリイメージ)を表すアドレス空間時間図の水平スライスのセットです。特定の平面では、すべてイベント(またはメモリ値)の同時性です。Absolute Timeという概念があり、すべてのスレッドがどのメモリ値が同時であるかについて合意します。SCでは、すべての瞬間に1つのメモリイメージのみが共有されます。つまり、すべてのプロセッサがメモリイメージ(つまり、メモリの総計コンテンツ)に同意します。これは、すべてのスレッドがすべてのメモリ位置に対して同じ値のシーケンスを表示するだけでなく、すべてのプロセッサーが同じ値の組み合わせすべての変数の組み合わせを観察します。これは、すべてのメモリー操作(すべてのメモリー位置)がすべてのスレッドによって同じ合計順序で観察されることと同じです。

リラックスメモリモデルでは、各スレッドは独自の方法でアドレス空間時間をスライスします。唯一の制限は、すべてのスレッドが個々のメモリロケーションの履歴に同意する必要があるため、各スレッドのスライスが互いに交差しないことです、異なるスレッドのスライスは互いに交差する可能性があります。それをスライスする普遍的な方法はありません(address-space-timeの特権的な構成はありません)。スライスは平面(または線形)である必要はありません。これらはカーブすることができ、これにより、スレッドは、別のスレッドによって書き込まれた値を、書き込まれた順序から読み取ることができます。異なるメモリ位置の履歴は、互いに対して任意にスライド(またはストレッチ)できます表示時特定のスレッドによる。各スレッドは、どのイベント(または同等に、メモリ値)が同時であるかについて異なる意味を持ちます。1つのスレッドに同時のイベント(またはメモリ値)のセットは、別のスレッドに同時ではありません。したがって、緩和されたメモリモデルでは、すべてのスレッドが各メモリロケーションの同じ履歴(つまり、値のシーケンス)を引き続き観察しますが、異なるメモリイメージ(つまり、すべてのメモリロケーションの値の組み合わせ)を観察する場合があります。異なるメモリ位置は同じスレッドによって順番に書き込まれ、新しく書き込まれた2つの値は他のスレッドによって異なる順序で観察される場合があります。

[ウィキペディアの写真] Picture from Wikipedia

アインシュタインの特殊相対性理論に精通している読者は、私がほのめかしていることに気付くでしょう。ミンコフスキーの言葉をメモリモデルの領域に変換します。アドレス空間と時間はアドレス空間時間の影です。この場合、各オブザーバー(スレッド)は、イベント(メモリストア/ロード)の影を自分の世界線(時間軸)と同時性の平面(アドレス空間軸)に投影します。 。 C++ 11メモリモデルのスレッドは、オブザーバーに対応し、特別な相対性で互いに相対的に移動します。シーケンシャル整合性はガリレオ時空に対応します(つまり、すべてのオブザーバーが同意しますイベントの1つの絶対的な順序とグローバルな同時感覚で)。

記憶モデルと特殊相対性理論の類似点は、両方とも因果集合と呼ばれる部分的に順序付けられた一連のイベントを定義するという事実に由来します。一部のイベント(メモリストアなど)は、他のイベントに影響を与えることができます(ただし、影響は受けません)。 C++ 11スレッド(または物理学のオブザーバー)は、一連のイベント(つまり、完全に順序付けられたセット)のイベント(たとえば、異なるアドレスへのメモリロードとストア)にすぎません。

相対性理論では、すべてのオブザーバーが同意する時間的順序は「時間的」イベント(すなわち、原則としてより遅くなる粒子によって接続可能なイベント)の順序のみであるため、一部の順序は部分的に順序付けられたイベントの一見カオスな画像に復元されます真空中の光の速度よりも)。時間に関連するイベントのみが不変に順序付けられます。 物理学の時間、クレイグ・キャレンダー

C++ 11メモリモデルでは、これらのローカル因果関係を確立するために、同様のメカニズム(取得-リリース整合性モデル)が使用されます。

メモリの一貫性の定義とSCを放棄する動機を提供するために、 「メモリの一貫性とキャッシュの一貫性に関する入門書」 から引用します。

共有メモリマシンの場合、メモリ一貫性モデルは、そのメモリシステムのアーキテクチャ上見える動作を定義します。シングルプロセッサコアの正当性基準は、「1つの正しい結果」と「多くの不正な代替」の間で動作を分割します。これは、プロセッサのアーキテクチャにより、スレッドの実行が特定の入力状態をアウトオブオーダーコアであっても、明確に定義された単一の出力状態ただし、共有メモリ整合性モデルは、複数のスレッドのロードとストアに関係し、通常、多くの正しい実行複数の正しい実行の可能性は、ISAが複数のスレッドの同時実行を許可しているためです。

Relaxedまたはweakメモリ整合性モデルは、強力なモデルのほとんどのメモリ順序付けが不要であるという事実に基づいています。スレッドが10個のデータ項目を更新してから同期フラグを更新すると、プログラマーは通常、データ項目が互いに関して順番に更新されるかどうかは気にしませんが、フラグが更新される前にすべてのデータ項目が更新されるだけです(通常はFENCE命令を使用して実装されます)。 SCのパフォーマンスと正確さの両方を得るためにプログラマが「require」する順序のみ。たとえば、特定のアーキテクチャでは、FIFO書き込みバッファが各コアで使用され、コミットされた結果を保持します(キャッシュに結果を書き込む前にストアを破棄します。この最適化はパフォーマンスを向上させますが、SCに違反します。書き込みバッファは、ストアミスを処理するレイテンシを隠します。ストアは一般的であるため、それらのほとんどでストールを回避できることは重要です一定の利益。シングルコアプロセッサの場合、Aへの1つ以上のストアが書き込みバッファ内にある場合でも、アドレスAへのロードが最新のストアの値をAに返すようにすることで、書き込みバッファをアーキテクチャ上不可視にすることができます。これは通常、最新のストアの値をAからAへのロードにバイパスし、「最新」はプログラムの順序によって決定されるか、Aへのストアが書き込みバッファーにある場合にAのロードをストールすることによって行われます。 。複数のコアを使用する場合、各コアには独自のバイパス書き込みバッファがあります。書き込みバッファがない場合、ハードウェアはSCですが、書き込みバッファがある場合はそうではないため、書き込みバッファはマルチコアプロセッサでアーキテクチャ的に見えるようになります。

コアに入力された順序とは異なる順序でストアを出発させる非FIFO書き込みバッファがある場合、ストア-ストアの並べ替えが発生する可能性があります。これは、最初のストアが2番目のヒット中にキャッシュをミスした場合、または2番目のストアが以前のストアと合体できる場合(つまり、最初のストアの前)に発生する可能性があります。ロード/ロードの順序変更は、プログラムの順序に関係なく命令を実行する動的にスケジュールされたコアでも発生する場合があります。これは、別のコアでストアを並べ替えるのと同じように動作します(2つのスレッド間でインターリーブの例を考えられますか?)。後のストアで以前のロードを並べ替える(ロードストアの並べ替え)と、保護するロックを解除した後に値をロードするなど、多くの不正な動作が発生する可能性があります(ストアがロック解除操作の場合)。ストア順の並べ替えは、一般的に実装されているFIFO書き込みバッファのローカルバイパスにより、すべての命令をプログラム順に実行するコアでも発生する場合があることに注意してください。

キャッシュの一貫性とメモリの一貫性は時々混同されるため、次の引用も参考にしてください。

一貫性とは異なり、cache coherenceはソフトウェアからは見えず、必須でもありません。Coherenceは、共有メモリシステムのキャッシュを、シングルコアシステムのキャッシュと同じくらい機能的に見えないようにします。プログラマーは、ロードとストアの結果を分析することでシステムにキャッシュがあるかどうか、またどこにキャッシュがあるかを判断できません。これは、正しい一貫性により、キャッシュが新しいまたは異なるfunctional動作を有効にしないためですタイミング情報)を使用してキャッシュ構造を推測するキャッシュコヒーレンスプロトコルの主な目的は、すべてのメモリロケーションに対してシングルライターマルチリーダー(SWMR)不変を維持することです。一貫性は、一貫性がメモリロケーションごとにで指定されるのに対し、一貫性はallメモリロケーションに関して指定されることです。

私たちのメンタルピクチャを続けると、SWMR不変量は、任意の場所に最大1つの粒子が存在するという物理的要件に対応しますが、任意の場所の観測者の数に制限はありません。

321
Ahmed Nassar

これは今では数年前の質問ですが、非常に人気があるので、C++ 11メモリモデルについて学ぶための素晴らしいリソースを言及する価値があります。私はこれをもう一つの完全な答えにするために彼の話をまとめることに意味がありません、しかし、これが実際に規格を書いた人であるとすれば、私はそれが話を見る価値があると思います。

Herb Sutterは、Channel 9サイトで入手可能な "atomic <> Weapons"というC++ 11メモリモデルについての3時間の長い話をしています - part 1part 2 。この講演はかなり技術的なもので、次のトピックが含まれています。

  1. 最適化、人種、そして記憶モデル
  2. 発注 - 機能:取得およびリリース
  3. 注文方法 - ミューテックス、アトミック、フェンス
  4. コンパイラとハードウェアに関するその他の制限
  5. コード生成とパフォーマンス:x86/x 64、IA 64、POWER、ARM
  6. リラックスアトミック

この話はAPIについては詳しく説明していませんが、推論、背景、裏側、そして裏側で説明しています(POWERおよびARMがサポートされていないために緩やかな意味が標準に追加された同期ロードは効率的ですか?).

97
eran

これは、標準がマルチスレッドを定義し、マルチスレッドのコンテキストで何が起こるかを定義していることを意味します。もちろん、人々はさまざまな実装を使っていましたが、ホームロールのstringクラスを使うことができるのになぜstd::stringを持つべきかを尋ねるようなものです。

POSIXスレッドやWindowsスレッドについて話しているとき、これは実際にはx86スレッドについて話しているのと同じくらい幻想的です。同時に実行するのはハードウェア関数だからです。 C++ 0xメモリモデルは、x86、ARM、 _ mips _ 、またはその他のあらゆる方法で動作するかどうかを保証します。

73
Puppy

メモリモデルを指定しない言語の場合、言語およびプロセッサアーキテクチャで指定されたメモリモデルのコードを記述します。プロセッサは、パフォーマンスのためにメモリアクセスを並べ替えることができます。したがって、プログラムにデータの競合がある場合(データの競合とは、複数のコア/ハイパースレッドが同じメモリに同時にアクセスできる場合です)プログラムは、プロセッサメモリモデルに依存しているため、クロスプラットフォームではありません。 IntelまたはAMDのソフトウェアマニュアルを参照して、プロセッサがメモリアクセスを並べ替える方法を確認してください。

非常に重要なことは、ロック(およびロックを伴う同時実行セマンティクス)は通常、クロスプラットフォームの方法で実装されることです。したがって、データレースのないマルチスレッドプログラムで標準ロックを使用している場合、don 'クロスプラットフォームメモリモデルについて心配する必要があります

興味深いことに、MicrosoftのC++コンパイラーは、C++のメモリモデルの不足に対処するC++拡張機能であるvolatileのセマンティクスを取得/リリースしています http://msdn.Microsoft.com/en-us/library/12a04hfd (v = vs.80).aspx 。ただし、Windowsがx86/x64でのみ動作することを考えると、それはさほど重要ではありません(IntelおよびAMDのメモリモデルにより、言語での獲得/解放セマンティクスの実装が簡単かつ効率的になります)。

54
ritesh

すべてのデータを保護するためにミューテックスを使用しているのであれば、本当に心配する必要はありません。ミューテックスは常に十分な順序付けと可視性の保証を提供してきました。

さて、もしあなたがアトミックまたはロックフリーのアルゴリズムを使ったのなら、メモリモデルについて考える必要があります。メモリモデルは、アトミックが順序付けと可視性の保証を提供する時期を正確に記述し、手でコード化された保証のためのポータブルフェンスを提供します。

以前は、アトミックはコンパイラ組み込み関数、またはそれより高いレベルのライブラリを使用して行われていました。フェンスは、CPU固有の命令(メモリバリア)を使用して行われていたはずです。

25
ninjalj

CおよびC++は、整形式プログラムの実行トレースによって定義されていました。

現在は、プログラムの実行トレースによって半分が定義され、同期オブジェクトの多くの順序付けによって事後的に定義されています。

つまり、これらの言語定義は、これらの2つのアプローチを混合する論理的な方法ではないので、まったく意味がありません。特に、ミューテックスまたはアトミック変数の破壊は明確に定義されていません。

0
curiousguy