PC開発とパフォーマンス-メモリレイテンシ

Herb Sutter (Exceptional C ++の作者、ISO C ++標準委員会の前長、 Mr。Free Lunch Is Overなど)はマイクロソフトで働いており、水曜日にアトミックレクチャーを行うこともあります。



私は最終的にそのようなものをヒットし、とても幸せでした。 賢い人を見たり聞いたりするのはいつも楽しいことです。

記録のために-ハーブに加えて、ライブOlexandrescuとライブWalter Bright( " D ")を見ました。



講義は「マシンアーキテクチャ:プログラミング言語が決してあなたに言ったことのないもの」と呼ばれ( ここではプレゼンテーションとビデオをダウンロードできます)、抽象化ペナルティの特定の部分であるメモリレイテンシについて説明しました。



私は講義の重要な考えについて簡単に話そうとします。 それは簡単で、明白で、千回言った。 私はもう一度アルファベットを繰り返すと思います-それは決して痛いことはありません。





レイテンシーと帯域幅について最小



帯域幅はチャネルの幅です。 1秒間にポンプできるデータの量、ALUを完全にロードするために送信できる命令の数など。

遅延とは、チャネルの長さ、つまり、要求したデータがどれだけの時間後に届くかです。 メモリから要求されたビットが何クロックサイクル後に来るか、何クロックサイクル後に命令の結果が準備できるか、コマンドがパイプラインの最後に行くときなどです。

そしてもちろん、それらはお互いに影響します。 結果が必要になるとすぐに、それ以上のことはありません。帯域幅全体が遅延のためにアイドル状態になります。 彼らはキャッシュにないメモリを要求しました-私たちは座って、メモリを待っています。 前の結果を必要とする命令を実行する場合は、その実装を待っています。 これにより、チャネルに「バブル」が作成され、それに応じて負荷が軽減されます。



プレゼンテーションのハーブは、石油パイプラインの例を使用していますが、それは非常に明白です。 1分間に大量の樽を汲み上げることができますが、各樽は数日間その目的地に行きます。 最も純粋な形式では、帯域幅と遅延。



実用的なポイントは、帯域幅は常に簡単に購入できるということです。 2つのプロセッサを配置し、一度に2倍のデータをメモリから取得し、最後に2つのコンピューターを配置します。 レイテンシーははるかに高価です-2人の女性は4.5ヶ月で赤ちゃんを出産せず、進歩とともにのみ前進しています-頻度の増加、要素のサイズの縮小、技術の変更など。



そして今、過去20年以上の間に、レイテンシーの成長はずっと遅くなっています。 特に-メモリのレイテンシ。



シャー、ハーブはそこにサインを持っていました...



                                 
                     1980 VAX-11 / 750 1980年以降の最新のデスクトップの改善 

クロック速度(MHz)6 3000 + 500x 

メモリサイズ(RAM、MB)2 2000 + 1000x 

メモリ帯域幅(MB / s)13 7000(読み取り)+ 540x 

                                         2000(書き込み)+ 150x 

メモリレイテンシ(ns)225〜70 + 3x 

メモリレイテンシ(サイクル)1.4 210 -150x(!!!!!!)




タブレットから、プロセッサが順調に成長し、メモリサイズが順調に成長し、メモリ帯域幅も圧倒的であることがはっきりとわかりますが、VAXは3倍しか改善されていないため、遅延があります。 対策に頼って(最後の行)-150回悪化しました。

つまり、キャッシュミスのコストは、最も重いプロセッサー命令よりも桁違いに高くなります。



80年代はシンプルでクールでした-メモリへのアクセスのコストは、計算命令(および一般的には浮動小数点)に匹敵するか、それ以下でした。

プロセッサ、ディスク、メモリがあり、プログラマはそれらを直接操作します。 コードはビートに対して透過的かつ予測可能に実行されます。



実際、鉄では、実際、すべてが異なっています。 メモリへのアクセス-数百ティック。 はい、キャッシュライン全体(32または64バイト)を一度に取得できますが、とにかく数百クロック待機します。 たとえば、1ミリ秒で、メモリ内のさまざまな場所に約10,000回移動することがわかります。 異なるクラスの100個のオブジェクト、それぞれで10個の仮想関数の呼び出し-すでに20 +ミリ秒の20%。 ゲーム開発者-非常に実数。 そして、一般的に言えば、メモリトラフィックは最も重要なものです。



そして、それはすべて記憶に関するものです。 ディスクに登った場合-すでに善悪の限界を完全に超えており、数千万ティックのレイテンシがあります。



それを処理する方法-もちろん、キャッシュと階層。 L1-2メジャー、L2-14メジャー、L3-約40としましょう。データ用に、命令用に別々に。

キャッシュの複雑なロジック、プロセッサのさまざまなメーカーのノウハウなど。

さらに、待っている人に依存しないものを成し遂げようとするためには、故障することが義務付けられています。

順不同の実行、レジスタの名前変更、必然的に強力な分岐予測は、できるだけ早くメモリへのアクセスと書き込みを開始するようにしてください。 ブランチが間違った方向に進むと、すぐに故障して破壊されてしまいます。

繰り返しますが、内部には長いコンベアがあります。 P4では、病理学的にも長かった-一度に最大25命令まで、順不同で100命令を探していました。 最新のプロセッサでは、パイプラインは小さくなっていますが、不透明です。



Sutterは、Itanium2では、キャッシュがプロセッサ領域の85%を占めると書いています。

Core Duoで-Googleで検索できませんでした。同じことを考えています。

別の10プラスパーセントは、故障、分岐予測、およびその他の効果のロジックです。

実際に何かを考えている実際のALUの数パーセントのままでした。



最新のプロセッサは計算機ではなく、x86命令の巨大なハードウェアエミュレータです。

これはすべて、プログラマーから待ち時間を隠すために必要です。 プロセッサとメモリしかない場合でも、80年代にプログラミングを続けることができ、必要に応じて安価にメモリにアクセスできます。 古いコードを実行し続けるために、新しいコードも作成できるように改善されています。



そしてまだ-私たちは150倍の速度低下を隠そうとしています! プログラマには気付かれません! データ構造を変更せずに! 彼が命令の実行順序の変化に気付かないように!



もちろん、このアクティビティが最適になることはありません。



ある意味、プログラマーはエルフの国に住んでいるという事実から、サッターは2つの実際的な結果をもたらします。



まず、プログラムの正確さに影響します。



Satterのお気に入りのマルチスレッドで、メモリへの読み取りと書き込みのシーケンスに関して仮定が行われるすべての場所。

メモリへのintの書き込みがアトミックであると仮定して、ロックフリースレッドインタラクションの実行を開始すると、自分自身を傷つけます。



例:



Thread1:

flag1 = 1;

if (flag2 != 0) { …}

// enter critical section



Thread2:

flag2 = 1;

if (flag1 != 0) { …}

// enter critical section











Thread1はまずflag1-共有リソースが必要なフラグを設定し、2番目のリソースが別のスレッドでビジーかどうかを確認します。 flag2は、flag1を設定した後にのみチェックされると想定されます(他のスレッドでビジー状態にあるクリティカルセクションに入らないようにするため)。

そして、総計があります-flag1のメモリ読み取りは、順序が正しくないため非常に早く発生し(正式には、この読み取りは何にも依存しないため、早期に実行できます)、同期は行われません。

したがって、正直にロックする必要があります。 変数の値を反映するものとしてメモリに依存することは不可能です。



2番目の最も楽しい部分は、もちろんパフォーマンスです。



長い間、メモリの大部分は低速でした。 これは主に帯域幅ではなく遅延によるものです。 ランダムメモリ読み取りは、計算のクラウド全体よりもはるかに高価です。 局所性は、あらゆる規模で重要です。



ところで、実際のプログラムで「ランダム」であるものは、不透明なキャッシュ階層のためにひどくぼやけています。

たくさん使用されている場合、とにかくキャッシュにあるようです。 一方、異なる時点での実際のワーキングセットはいくらですか-推定値ではなく、実際の値です。

また、プロセッサごとに異なります。 そして、それはデータに非常に依存しています。 そして、最もクールなことはそれも測定することです!

彼は例を合成例に減らしました-キャッシュに収まり始めました。 コンバージョン



幸いなことに(残念ながら?)、キャッシュミスの価格は非常に高いため、深刻な問題は厚いレイヤーで測定できます。

順次アクセス(帯域幅を測定)に対するランダムアクセス(遅延を測定)の速度は、桁違いに異なります。 これは、std :: vectorとstd :: listの違いです。

さらに悪いことに、std :: vector <T>とstd :: vector <T *>(これは誰もが知っているように、Javaまたは.netのオブジェクトの配列)の違いかもしれません。



最後に-あなたは常に記憶について考える必要があります。 局所性とコストの両方。

記憶にあるかどうかを測定します。 ランダムアクセスの場合-生産的に考えて解決できます。 そして、フットプリントにあるとき-それも起こります。



gamedeff では、そのような局所性の闘争の良い例がここで説明されました



しかし、正確に測定および予測することは依然として不可能です。 すべてが非常に厚く、非線形で不透明です。 理解できないロジックと、さらに悪いことに、理解できない読み込みを備えた大きなマシンが動作しています。 ネットワークはバックグラウンドで実行され、すべてを混乱させます。 またはインデクサー、神は禁じます。



そして、PCの世界でそれをどうするかわかりません



許してください、私はゲームだけを書いたアプリケーションから、私のお気に入りのゲーム開発者の例だけでプラットフォームを推論し、比較します。



一方では、より多くの制御が必要です。 アクセス時間を保証できる明確なキャッシュの場所を用意してください。 最初のコンテキスト切り替え時にキャッシュが損なわれないことを保証します。

たとえば、完全に異なるハードウェアが存在するコンソールの世界では、すべてがどれほど優れているかについて話すのは簡単です。 SPU、256 kbの完全に管理された非常に高速なローカルメモリ、広い(レイテンシを隠すため)DMAパケットでメインメモリへの要求をクリアします。 または、Xbox360では、キャッシュの一部をしばらくロックし、GPUからレンダリングを要求することもできます。

これらのモデルはどれも、PCで最も純粋な形で回復することはありません。

多くのスレッドが同時に1つのプロセッサ上に存在します。各スレッドが256キロバイトのメモリを管理する場合、コンテキストスイッチを使用してアンロードおよびロードする必要があります。 これは、重くて長いコンテキストスイッチであり、通常はOSでも、セミアクティブスレッドでさえも実行します。

同じ理由でキャッシュのロックを許可しないでください。これは、コンテキストの切り替え中にキャッシュをメモリにバッファリングするか、他のアプリケーションから永久に取得することを意味します。 アクティブなもののみを削除すると、残りの処理が遅くなります。



さらに悪いことに、主要なアプリケーションには上限がありません。 10キロバイトと100メガバイトのドキュメントをロードできます。 Excelスプレッドシートのサイズは数千倍も異なる場合がありますが、コンソールのようにメモリに上限を設定することはありません。



さらに、鉄のセットとメモリの量の両方が常に異なり、ターゲットは粘り気です-「より少ないメモリを消費し、より速く動作します」。 そして、鉄はそれが機能する以上のものをエミュレートします。



下位互換性のない固定ハードウェア上のシステム内の1つのアプリケーションの寿命は、古いコードおよびその他の要件を備えた、無期限のハードウェア上でまだらにされたクラウドの寿命とは根本的に異なります。 よく見ると、世界は違うと思います。



そして、これは問題のほんの一部です。 基本的なものは後方互換性であり、「パフォーマンスと開発コスト」のバランスはコンソールとはまったく異なります。 しかし、これについては、後で何とか無限に書くことができます。



最後に、簡単な瞑想的なtsifirki(自宅のマシンで取りました):



浮動小数点数:0.5-4サイクル(1つのコアで)

L1アクセス(〜16-32 kb):〜2-3サイクル

L2アクセス(〜2-4 mb):〜15サイクル

ランダムメモリアクセス:〜200サイクル

プリフェッチによるシーケンシャルアクセス:〜2バイト/サイクル



男性と戦うために残っています。 このレベルでの抽象化の価格を理解し、脳をリラックスさせて80年代に生きさせないでください。




All Articles