並行処理のためのC++のメモリモデルと言えば、StroustrupのC++プログラミング言語、4版、セクション。 41.2.1は言う:
...(ほとんどの最新のハードウェアと同様)、マシンはWordより小さいものをロードまたは保存できませんでした。
ただし、数年前の私のx86プロセッサは、Wordよりも小さいオブジェクトを格納できます。例えば:
#include <iostream>
int main()
{
char a = 5;
char b = 25;
a = b;
std::cout << int(a) << "\n";
return 0;
}
最適化なしでは、GCCはこれを次のようにコンパイルします。
[...]
movb $5, -1(%rbp) # a = 5, one byte
movb $25, -2(%rbp) # b = 25, one byte
movzbl -2(%rbp), %eax # load b, one byte, not extending the sign
movb %al, -1(%rbp) # a = b, one byte
[...]
コメントは私によるものですが、総会はGCCによるものです。もちろん、問題なく動作します。
明らかに、ハードウェアはWordよりも小さいものは何もロードおよび格納できないと彼が説明したとき、私はStroustrupが何について話しているのか理解していません。私の知る限り、私のプログラムは以外は何もしませんWordよりも小さいオブジェクトをロードして格納します。
ゼロコストでハードウェアに優しい抽象化にC++が徹底的に焦点を当てているため、C++は習得が容易な他のプログラミング言語とは一線を画しています。したがって、Stroustrupがバス上の信号の興味深いメンタルモデルを持っている場合、またはこの種の他の何かを持っている場合は、Stroustrupのモデルを理解したいと思います。
は何ですかStroustrupは話していますか?
コンテキスト付きロングクォート
これは、より完全なコンテキストでのStroustrupの引用です。
リンカが[
char
タイプの変数]c
およびb
をメモリ内の同じWordに割り当て、マシンが(ほとんどの最新のハードウェアのように)割り当てられなかった場合にどうなるかを検討します。 Wordよりも小さいものをロードまたは保存します...明確に定義された適切なメモリモデルがない場合、スレッド1はb
およびc
を含むWordを読み取り、c
を変更します。 、Wordをメモリに書き戻します。同時に、スレッド2はb
でも同じことができます。次に、Wordを最初に読み取ることができたスレッドと、その結果を最後にメモリに書き戻すことができたスレッドが、結果を決定します。
追加の備考
Stroustrupがキャッシュラインについて話しているとは思わない。彼が私の知る限り、キャッシュコヒーレンシプロトコルは、ハードウェアI/O中を除いて、その問題を透過的に処理します。
プロセッサのハードウェアデータシートを確認しました。電気的には、私のプロセッサ(Intel Ivy Bridge)は、なんらかの16ビット多重化スキームによってDDR3Lメモリをアドレス指定しているようです。そのため、それが何であるかわかりません。でも、それがストロウストラップのポイントと関係があるかどうかははっきりしない。
ストロストルプは賢い人であり、卓越した科学者なので、彼が賢明なことをしているのは間違いない。私は混乱しています。
参照 この質問。 私の質問はいくつかの点でリンクされた質問に似ており、リンクされた質問への回答もここで役立ちます。ただし、私の質問は、C++をそのように動機づけるハードウェア/バスモデルにも当てはまり、それによりStroustrupは彼が書いたものを書き込みます。私はC++標準が正式に保証するものに関してのみ答えを求めるのではなく、C++標準がそれを保証する理由を理解したいと思います。根底にある考えは何ですか?これも私の質問の一部です。
X86 CPUは1バイトの読み取りと書き込みができるだけでなく、最近のすべての汎用CPUがそれを処理できます。さらに重要なのは、最近のほとんどのCPU(x86、ARM、MIPS、PowerPC、SPARCを含む)は、1バイトをアトミックに読み書きできることです。
Stroustrupが何を言っていたのか分かりません。 Crayのように、8ビットのバイトアドレス指定ができないWordのアドレス指定可能なマシンがいくつかありました。PeterCordesが述べたように、初期のAlpha CPUはバイトのロードとストアをサポートしていませんでしたが、今日ではバイトができない唯一のCPUロードとストアは、ニッチアプリケーションで使用される特定のDSPです。彼が最新のCPUのほとんどにアトミックバイトロードとストアがないことを意味すると仮定しても、これはほとんどのCPUには当てはまりません。
ただし、単純なアトミックなロードとストアは、マルチスレッドプログラミングではあまり役に立ちません。また、通常、順序の保証と、読み取り、変更、書き込み操作をアトミックにする方法も必要です。もう1つの考慮事項は、CPUにバイトのロードとストアの命令がある場合でも、コンパイラーがそれらを使用する必要がないことです。たとえば、コンパイラは、Stroustrupが記述するコードを引き続き生成し、最適化として単一のWordロード命令を使用してb
とc
の両方をロードできます。
したがって、明確に定義されたメモリモデルは必要ですが、コンパイラが期待するコードを生成するように強制されている場合にのみ、問題は、最新のCPUがWordよりも小さいものをロードまたは格納できないことではありません。
Stroustrupが「Word」によって何を意味するのかわからない。多分それはマシンのメモリストレージの最小サイズですか?
とにかく、すべてのマシンが8ビット(BYTE)解像度で作成されたわけではありません。実際、私は、Eric S. Raymondによる、コンピューターの歴史のいくつかを説明したこの素晴らしい記事をお勧めします。 http://www.catb.org/esr/faqs/things-every-hacker-once-knew/ =
「... 36ビットアーキテクチャがC言語のいくつかの不幸な機能を説明したことも一般に知られていました。元のUnixマシンであるPDP-7は、大きな36ビットのハーフワードに対応する18ビットワードを備えていましたこれらは、より自然に6つの8進数(3ビット)の数字で表されました。」
これは正しいです。 x86_64 CPUは、元のx86 CPUと同様に、rspから(この場合は64ビット)ワードよりも小さいものを読み書きできません。メモリに。また、キャッシュをバイパスする方法はありますが(特に以下を参照)、キャッシュライン全体を下回る読み取りや書き込みは通常行われません。
ただし、このコンテキストでは、Stroustrupは潜在的なデータ競合(観察可能なレベルでの原子性の欠如)を指します。あなたが言及したキャッシュ一貫性プロトコルのため、この正当性の問題はx86_64では無関係です。つまり、はい、CPUはWord転送全体に制限されます。butこれは透過的に処理され、プログラマーとしてのあなたは通常心配する必要はありません。それについて。実際、C++ 11以降のC++言語では、保証異なるメモリロケーションでの同時操作には明確に定義された動作、つまり、期待するだろう。ハードウェアがこれを保証していなかったとしても、実装はおそらくより複雑なコードを生成することによって方法を見つける必要があります。
とはいえ、2つの理由から、単語全体またはキャッシュラインさえも常にマシンレベルで頭の後ろにあるという事実を維持することは良い考えである可能性があります。
volatile
キーワードは、このような不適切な最適化を防ぐために不可欠です。これは、非常に悪いデータ構造の例です。ファイルからテキストを解析する16のスレッドがあるとします。各スレッドには、0〜15のid
があります。
// shared state
char c[16];
FILE *file[16];
void threadFunc(int id)
{
while ((c[id] = getc(file[id])) != EOF)
{
// ...
}
}
各スレッドは異なるメモリ位置で動作するため、これは安全です。ただし、これらのメモリの場所は通常、同じキャッシュラインに存在するか、最大で2つのキャッシュラインに分割されます。次に、キャッシュコヒーレンシプロトコルを使用して、c[id]
へのアクセスを適切に同期します。そして、ここに問題があります。これは、すべてのotherスレッドがc[id]
で何かを行う前に、キャッシュラインが排他的に利用可能になるまで待機することを強制するためです。キャッシュラインを「所有する」コアですでに実行されています。いくつかを仮定します。 16、コア、キャッシュコヒーレンシは通常、常にキャッシュラインをあるコアから別のコアに転送します。明らかな理由により、この影響は「キャッシュラインピンポン」として知られています。それは恐ろしいパフォーマンスのボトルネックを作り出します。これは、false sharingの非常に悪いケース、つまり、実際に同じ論理メモリ位置にアクセスせずに物理キャッシュラインを共有するスレッドの結果です。
これとは対照的に、特にfile
配列が独自のキャッシュラインに存在することを確認する追加の手順を実行した場合、ポインターは読み取り専用であるため、それを使用してもパフォーマンスの観点からは完全に無害です(x86_64上)。から、ほとんどの場合。この場合、複数のコアがキャッシュラインを読み取り専用として「共有」できます。いずれかのコアがキャッシュラインに書き込もうとする場合にのみ、他のコアに、排他的アクセスのためにキャッシュラインを「占有」することを通知する必要があります。
(CPUキャッシュにはさまざまなレベルがあり、いくつかのコアが同じL2またはL3キャッシュを共有している可能性があるため、これは大幅に簡略化されていますが、問題の基本的な考え方がわかるはずです。)
著者は、スレッド1とスレッド2がread-modify-writes(ソフトウェアではなく、ソフトウェアがバイトサイズの2つの別々の命令を実行する状況に陥る、ラインロジックがどこかで読み取りを行う必要がある)に懸念を持っているようです。 modify-write)は、理想的な読み取り、変更、書き込み、読み取り、変更、書き込みの代わりに、読み取り、読み取り、変更、変更、書き込み、またはその他のタイミングになり、変更前のバージョンと最後に書き込まれたバージョンの両方が優先されます。読み取り読み取り変更変更書き込み書き込み、または読み取り変更読み取り変更書き込み書き込みまたは読み取り変更読み取り書き込み変更書き込み。
懸念事項は、0x1122で開始し、1つのスレッドが0x33XXにしたい、もう1つのスレッドが0xXX44にしたいですが、たとえば、読み取り、読み取り、変更、書き込み、書き込みの場合、最終的に0x1144または0x3322になりますが、0x3344ではありません。
正気な(システム/ロジック)デザインには、このような汎用プロセッサでは問題がないことは確かです。このようなタイミングの問題があるデザインに取り組みましたが、ここでは、まったく異なるシステムデザインについて説明します。さまざまな目的のため。読み取り-変更-書き込みは、正常な設計では十分な距離に及ばず、x86は正常な設計です。
読み取り-変更-書き込みは、関係する最初のSRAM(理想的にはC86でコンパイルされたマルチスレッドプログラムを実行できるオペレーティングシステムでx86を通常の方法で実行する場合はL1)の近くで発生し、RAMが数クロックサイクル以内に発生します。理想的にはバスの速度で。そして、ピーターが指摘したように、これは、プロセッサコアとキャッシュの間の読み取り-変更-書き込みではなく、キャッシュ内でこれを経験するキャッシュライン全体であると見なされます。
マルチコアシステムでも「同時に」という概念が必ずしも同時になるとは限りません。パフォーマンスは最初から最後まで並列であることに基づいていないため、最終的にはシリアル化されますが、バスの維持に基づいています。ロードされました。
引用はメモリ内の同じワードに割り当てられた変数を言っているので、それは同じプログラムです。 2つの別個のプログラムがそのようなアドレス空間を共有することはありません。そう
これを試してみてください。1つがアドレス0xnnn00000に書き込み、もう1つがアドレス0xnnnn00001に書き込むマルチスレッドプログラムを作成し、それぞれが書き込みを行ってから、1回の読み取りよりも同じ値の複数回の書き込みを行ってください。彼らが書いたバイト、そして異なる値で繰り返す。しばらくの間、時間/日/週/月間実行します。システムを起動したかどうかを確認します。実際の書き込み手順については、アセンブリを使用して、要求どおりに動作していることを確認してください(C++や、同じWordにこれらの項目を配置しないと主張または主張するコンパイラーではありません)。遅延を追加してより多くのキャッシュエビクションを可能にすることができますが、「同時に」衝突する可能性は低くなります。
0xNNNNFFFFFや0xNNNN00000のような境界(キャッシュなど)の両側に座っていないことを保証する限り、この例では、0xNNNN00000や0xNNNN00001のようなアドレスへの2バイトの書き込みを分離して、命令が戻って戻ってくるかどうかを確認します。読み取り読み取り変更変更書き込み書き込み。 2つの値がループごとに異なるようにテストをラップし、後で必要に応じて任意の遅延でWord全体を読み取り、2つの値を確認します。数日/数週間/数か月/数年繰り返して、失敗するかどうかを確認します。プロセッサの実行とマイクロコード機能を読んで、この命令シーケンスで何が行われるかを確認し、必要に応じて、プロセッサコアの反対側の数クロック程度のサイクルでトランザクションを開始しようとする別の命令シーケンスを作成します。
編集
引用符の問題は、これがすべて言語とその使用に関することです。 「ほとんどの最新のハードウェアのように」は、トピック/テキスト全体を扱いにくい位置に配置します。あいまいすぎるため、片方は、他のすべてを真にするために当てはまるケースを1つ見つけることです。私が1つのケースを見つけた場合、残りのすべてが真実ではないと主張することができます。 Wordをそのような混乱のようなものとして使用すると、可能な限り刑務所のないカードから抜け出すことができます。
実際のところ、データのかなりの割合が8ビット幅のメモリのDRAMに格納されています。通常、8ビット幅なので、64ビット幅で一度に8つのデータにアクセスするため、データにアクセスしません。数週間/数か月/数年/数十年の間、このステートメントは正しくありません。
大きい方の引用は「同時に」と表示され、次に読み取り...最初に、書き込み...最後、まあ最初と最後、そして同時に意味をなさないのですが、並列ですか、直列ですか?全体としてのコンテキストは、上記の読み取り、読み取り、変更、変更、書き込み、書き込みのバリエーションに関係し、最後に1つの書き込みがあり、その読み取りによって、両方の変更が行われたかどうかが判断されます。ほぼ同時に、「ほとんどの最新のハードウェアのように」、実際には別々のコア/モジュールで並列に開始するものが、メモリ内の同じフリップフロップ/トランジスタを目的としている場合、最終的にシリアル化されます。他が先に行くのを待つ必要があります。物理学に基づいているので、これが今後数週間/数か月/数年間で正しくないことはないと思います。
StroustrupはではないネイティブのWordサイズよりも小さいロードおよびストアを実行できるマシンはないと述べ、マシンできませんでした。
これは最初は意外と思われるかもしれませんが、難解なことではありません。
まず、キャッシュ階層を無視し、後でそれを考慮します。
CPUとメモリの間にキャッシュがないと仮定します。
メモリの大きな問題はdensityであり、可能な限り多くのビットを最小領域に配置しようとします。
電気設計の観点から、バスをできるだけ広く公開することが便利であることを実現するために(これは一部の電気信号の再利用に有利です)、特定の詳細は調べていませんが)。
したがって、大きなメモリが必要なアーキテクチャ(x86など)またはシンプルな低コスト設計が望ましい(たとえば、RISCマシンが関係する)場合、メモリバスは最小のアドレス指定可能なユニット(通常、バイト)。
プロジェクトの予算とレガシーに応じて、メモリはより広いバスを単独で、または特定のユニットを選択するためにいくつかのサイドバンド信号とともに公開できます。
これは実際にはどういう意味ですか?
DDR3 DIMMのデータシート を見ると、64DQ0–DQ63データを読み書きするためのピン。
これは、64ビット幅、一度に8バイトのデータバスです。
この8バイトのことは、インテルが最適化マニュアルのWCセクションでデータを64bytesバッファーを埋めます(ここではキャッシュを無視していますが、これはキャッシュラインが書き戻される方法に似ています)、8バイトのバーストで(できれば、継続的に)行います。
これは、x86がQWORDS(64ビット)しか書き込めないということですか?
いいえ、同じデータシートに、各DIMMにDM0–DM7、DQ0–DQ7およびDQS0–DQS7信号は、64ビットデータバスの8バイトのそれぞれをマスク、ダイレクト、およびストローブします。
したがって、x86はバイトをネイティブかつアトミックに読み書きできます。
しかし、これがすべてのアーキテクチャに当てはまるわけではないことが簡単にわかります。
たとえば、VGAビデオメモリはDWORD(32ビット)でアドレス指定可能で、8086のバイトアドレス指定可能な世界に適合させることで、乱雑なビットプレーンが発生しました。
DSPのような一般的な特定用途のアーキテクチャでは、ハードウェアレベルでバイトアドレス指定可能なメモリを使用できませんでした。
ひねりがあります。これまでメモリデータバスについて説明しましたが、これは可能な限り最下位の層です。
一部のCPUは、Wordアドレス指定可能メモリの上にバイトアドレス指定可能メモリを構築する命令を持つことができます。
どういう意味ですか?
Wordの小さな部分をロードするのは簡単です。残りのバイトを破棄するだけです!
残念ながら、プロセッサが整列されていないバイトのロードをシミュレートし、それを含む整列されたワードを読み取って保存する前に結果を回転させたアーキテクチャの名前を思い出せません(まったく存在していたとしても!)レジスタに。
ストアの場合、問題はより複雑になります。更新したばかりのWordの部分を単に書き込むことができない場合は、変更されていない残りの部分も書き込む必要があります。
CPUまたはプログラマーは、古いコンテンツを読み取り、更新して、書き戻す必要があります。
これは読み取り-変更-書き込み操作であり、原子性を論じるときのコアコンセプトです。
検討してください:
_/* Assume unsigned char is 1 byte and a Word is 4 bytes */
unsigned char foo[4] = {};
/* Thread 0 Thread 1 */
foo[0] = 1; foo[1] = 2;
_
データ競合はありますか?
バイトを書き込むことができるため、これはx86では安全ですが、アーキテクチャができない場合はどうなりますか?
どちらのスレッドもwholefoo
配列を読み取って変更し、書き戻す必要があります。
pseudo-Cでは、これは
_/* Assume unsigned char is 1 byte and a Word is 4 bytes */
unsigned char foo[4] = {};
/* Thread 0 Thread 1 */
/* What a CPU would do (IS) What a CPU would do (IS) */
int tmp0 = *((int*)foo) int tmp1 = *((int*)foo)
/* Assume little endian Assume little endian */
tmp0 = (tmp0 & ~0xff) | 1; tmp1 = (tmp1 & ~0xff00) | 0x200;
/* Store it back Store it back */
*((int*)foo) = tmp0; *((int*)foo) = tmp1;
_
これで、Stroustrupが何を話しているのかがわかります。2つのストア*((int*)foo) = tmpX
が互いに妨害していることを確認するには、この可能な実行シーケンスを検討してください。
_int tmp0 = *((int*)foo) /* T0 */
tmp0 = (tmp0 & ~0xff) | 1; /* T1 */
int tmp1 = *((int*)foo) /* T1 */
tmp1 = (tmp1 & ~0xff00) | 0x200; /* T1 */
*((int*)foo) = tmp1; /* T0 */
*((int*)foo) = tmp0; /* T0, Whooopsy */
_
C++にメモリモデルがない場合これらの種類の迷惑は実装固有の詳細であり、C++はマルチスレッド環境で役に立たないプログラミング言語のままになります。
おもちゃの例に描かれている状況がどれほど一般的であるかを考えると、Stroustrupは明確に定義されたメモリモデルの重要性を強調しました。
メモリモデルを形式化することは大変な作業であり、それは疲れ、エラーが発生しやすく、抽象的なプロセスであるため、prideもStroustrupの言葉。
私はC++メモリモデルをブラッシュアップしていませんが、さまざまな配列要素を更新しています 問題ありません 。
それは非常に強力な保証です。
キャッシュは省略しましたが、少なくともx86の場合は、実際には何も変わりません。
x86はキャッシュを介してメモリに書き込み、キャッシュは64バイトの行で削除されます。
内部的に、各コアは、ロード/ストアがライン境界を超えない限り(たとえば、その終わり近くに書き込むことにより)、任意の位置でラインをアトミックに更新できます。
これは、データを自然に調整することで回避できます(それを証明できますか?)。
マルチコード/ソケット環境では、キャッシュコヒーレンシプロトコルにより、一度にCPUのみがメモリのキャッシュされたライン(ExclusiveまたはModified状態のCPU)に自由に書き込むことができるようになります。
基本的に、プロトコルのMESIファミリは、DBMSを見つけたロックと同様の概念を使用します。
これは、書き込み目的で、異なるCPUに異なるメモリ領域を「割り当てる」という効果があります。
そのため、上記の議論には実際には影響しません。