CPUキャッシュを最大限に活用してパフォーマンスを向上させるコードをどのように記述しますか?


159

これは主観的な質問のように聞こえるかもしれませんが、私が探しているのは、これに関連して遭遇した特定のインスタンスです。

  1. コード、キャッシュを効果的/キャッシュフレンドリーにする方法(キャッシュヒットを増やし、キャッシュミスをできるだけ少なくする)?両方の観点から、データキャッシュとプログラムキャッシュ(命令キャッシュ)、つまり、データ構造とコード構成に関連する、コード内のどの部分を、キャッシュを効果的にするために注意する必要があります。

  2. コードキャッシュを効果的にするために、使用/回避しなければならない特定のデータ構造があるか、またはその構造のメンバーにアクセスする特定の方法などがあるか。

  3. この問題については、プログラムの構造(if、for、switch、break、gotoなど...)、コードフロー(ifの内部、ifの内部などの場合)をフォロー/回避する必要がありますか?

キャッシュを効率的なコードにすることに関する個々の経験を聞くのを楽しみにしています。プログラミング言語(C、C ++、アセンブリなど)、ハードウェアターゲット(ARM、Intel、PowerPCなど)、OS(Windows、Linux、S ymbianなど)などを使用できます。 。

多様性はそれを深く理解するのに役立ちます。


1
イントロとして、このトークは良い概要を示しますyoutu.be/BP6NxVxDQIs
schoetbi

上記の短縮URLはもう機能していないようです。これはトークへの完全なURLです:youtube.com/watch
v

回答:


119

キャッシュは、CPUがメモリ要求が満たされるのを待機してストールする回数を減らし(メモリレイテンシを回避)、2番目の効果として、転送する必要のあるデータ全体の量を減らすためにあります(メモリ帯域幅)。

メモリフェッチレイテンシの影響を回避するための手法は、通常、最初に検討するべきものであり、時には長い道のりを助けます。限られたメモリ帯域幅は、特にマルチコアやマルチスレッドアプリケーションで多くのスレッドがメモリバスを使用したい場合の制限要因でもあります。後者の問題への対処には、さまざまなテクニックが役立ちます。

空間的な局所性を改善することは、キャッシュにマップされた後、各キャッシュラインが完全に使用されることを保証することを意味します。さまざまな標準ベンチマークを調べたところ、キャッシュラインが追い出される前に、それらの驚くべき大部分がフェッチされたキャッシュラインの100%を使用できないことがわかりました。

キャッシュラインの使用率を改善すると、3つの点で役立ちます。

  • これは、より有効なデータをキャッシュに収める傾向があり、実質的に有効なキャッシュサイズが増加します。
  • 同じキャッシュラインにより多くの有用なデータを収める傾向があり、要求されたデータがキャッシュで見つかる可能性が高くなります。
  • フェッチ数が減るため、メモリ帯域幅の要件が軽減されます。

一般的な手法は次のとおりです。

  • より小さいデータ型を使用する
  • データを整理して位置合わせの穴を避けます(サイズを小さくして構造体メンバーをソートするのも1つの方法です)。
  • 標準の動的メモリアロケータに注意してください。これにより、ウォームアップ時にホールが発生し、メモリ内のデータが分散する可能性があります。
  • 隣接するすべてのデータが実際にホットループで使用されていることを確認してください。それ以外の場合は、データ構造をホットコンポーネントとコールドコンポーネントに分割して、ホットループがホットデータを使用することを検討してください。
  • 不規則なアクセスパターンを示すアルゴリズムとデータ構造を避け、線形データ構造を優先します。

また、キャッシュを使用する以外にも、メモリの待ち時間を隠す方法があることに注意してください。

最近のCPUには、多くの場合、1つ以上のハードウェアプリフェッチャーがあります。彼らはキャッシュのミスを訓練し、規則性を見つけようとします。たとえば、後続のキャッシュラインを数回ミスした後、ハードウェアプリフェッチャーはキャッシュラインをキャッシュにフェッチし始め、アプリケーションのニーズを予測します。通常のアクセスパターンを使用している場合、ハードウェアプリフェッチャーは通常、非常に優れています。また、プログラムが通常のアクセスパターンを表示しない場合は、自分でプリフェッチ命令を追加することにより、状況を改善できます。

常にキャッシュでミスする命令が互いに近くに発生するような方法で命令を再グループ化すると、CPUがこれらのフェッチをオーバーラップして、アプリケーションが1つのレイテンシヒットしか維持できない場合があります(メモリレベルの並列処理)。

全体的なメモリバスの負荷を減らすには、いわゆる一時的な局所性に対処する必要があります。つまり、キャッシュから削除されていないデータを再利用する必要があります。

同じデータにアクセスするループをマージし(ループフュージョン)、タイル化またはブロック化と呼ばれる書き換え技術を使用して、これらの余分なメモリフェッチを回避しようとします。

この書き換え演習にはいくつかの経験則がありますが、プログラムのセマンティクスに影響を及ぼさないようにするには、通常、ループ搬送データの依存関係を慎重に検討する必要があります。

これらは、通常、2番目のスレッドを追加した後、スループットの向上の多くが見られないマルチコアの世界で実際に見返りがあるものです。


5
さまざまな標準ベンチマークを調べたところ、キャッシュラインが追い出される前に、それらの驚くべき大部分がフェッチされたキャッシュラインの100%を使用できないことがわかりました。どのようなプロファイリングツールがこの種の情報を提供し、どのようにして提供するのですか?
Dragon Energy、

「データを整理して位置合わせの穴を避けます(サイズを小さくして構造体メンバーをソートするのが1つの方法です)」-コンパイラーがこれ自体を最適化しないのはなぜですか?コンパイラーが常に「サイズを小さくしてメンバーをソートする」ことができないのはなぜですか?メンバーを並べ替えないでおく利点は何ですか?
javapowered

私は起源を知りませんが、1つには、メンバーの順序がネットワーク通信(たとえば、構造体全体を1バイトずつWeb経由で送信する場合)において重要です。
Kobrar

1
@javapowered言語によっては、コンパイラーがそれを実行できる場合がありますが、実行できるかどうかはわかりません。Cでこれを行うことができない理由は、名前ではなくベースアドレス+オフセットでメンバーをアドレス指定することが完全に有効であるためです。つまり、メンバーを並べ替えるとプログラムが完全に壊れてしまいます。
Dan Bechard、2018

56

これに対する答えがこれ以上ないことは信じられません。とにかく、1つの古典的な例は、多次元配列を「裏返しに」反復することです。

pseudocode
for (i = 0 to size)
  for (j = 0 to size)
    do something with ary[j][i]

これがキャッシュ非効率である理由は、単一のメモリアドレスにアクセスすると、最新のCPUがメインメモリから「近い」メモリアドレスをキャッシュラインにロードするためです。内部ループの配列の "j"(外部)行を反復処理しているため、内部ループを通過するたびに、キャッシュラインがフラッシュされ、[に近いアドレスのラインがロードされます。 j] [i]エントリ。これを同等のものに変更した場合:

for (i = 0 to size)
  for (j = 0 to size)
    do something with ary[i][j]

それははるかに速く実行されます。


9
大学に戻ると、行列の乗算に関する課題がありました。その正確な理由により、「列」行列の転置を最初に取り、行ごとに列ではなく列ごとに乗算する方が高速であることがわかりました。
ykaganovich

11
実際、最近のコンパイラーのほとんどは、最適化をオンにして自分でこれを理解できます
Ricardo Nolde

1
@ykaganovichこれは、Ulrich Dreppersの記事の例でもあります。lwn.net/ Articles
Simon Stender

これが常に正しいかどうかはわかりません。アレイ全体がL1キャッシュ(多くの場合32k!)に収まる場合、両方の注文で同じ数のキャッシュヒットとミスが発生します。おそらく、メモリのプリフェッチが何らかの影響を与えると思います。もちろん修正してうれしいです。
Matt Parkins、2016年

順序が重要でない場合、誰がこのコードの最初のバージョンを選択するでしょうか?
silver_rocket 2018

45

基本的なルールは実際にはかなり単純です。トリッキーになるのは、コードへの適用方法です。

キャッシュは、時間的局所性と空間的局所性の2つの原則に基づいて機能します。前者は、特定のデータチャンクを最近使用した場合、おそらくすぐに再び必要になるという考えです。後者は、最近アドレスXでデータを使用した場合、おそらくすぐにアドレスX + 1が必要になることを意味します。

キャッシュは、最近使用されたデータのチャンクを記憶することにより、これに対応しようとします。これは、通常128バイト程度のサイズのキャッシュラインで動作するため、1バイトしか必要ない場合でも、それを含むキャッシュライン全体がキャッシュにプルされます。したがって、後で次のバイトが必要になった場合、それはすでにキャッシュにあります。

これは、独自のコードでこれらの2つの形式の局所性を可能な限り活用することを常に望んでいることを意味します。メモリ全体を飛び越えないでください。1つの小さな領域でできる限り多くの作業を行い、次に次の領域に進み、できる限り多くの作業を行います。

簡単な例は、1800の答えが示した2D配列トラバーサルです。一度に1行ずつトラバースすると、メモリが順番に読み取られます。列ごとに行うと、1つのエントリを読み取ってから、完全に異なる場所(次の行の先頭)にジャンプし、1つのエントリを読み取ってから、もう一度ジャンプします。そして、最終的に最初の行に戻ると、それはもはやキャッシュにありません。

同じことがコードにも当てはまります。ジャンプまたは分岐は、キャッシュの使用効率が低いことを意味します(命令を順番に読み取るのではなく、別のアドレスにジャンプするため)。もちろん、小さなifステートメントはおそらく何も変更しません(数バイトしかスキップしないので、最終的にはキャッシュされた領域内に収まります)が、関数呼び出しは通常、完全に異なる場所にジャンプすることを意味しますキャッシュされない可能性のあるアドレス。最近呼ばれていない限り。

しかし、通常、命令キャッシュの使用はそれほど問題にはなりません。通常心配する必要があるのはデータキャッシュです。

構造体またはクラスでは、すべてのメンバーが隣接して配置されています。これは良いことです。配列では、すべてのエントリも隣接して配置されます。リンクされたリストでは、各ノードは完全に異なる場所に割り当てられますが、これは悪いことです。一般に、ポインターは無関係なアドレスを指す傾向があるため、逆参照するとキャッシュミスになる可能性があります。

また、複数のコアを活用する場合は、非常に興味深いものになる可能性があります。通常、1つのCPUだけが一度にL1キャッシュに特定のアドレスを持つことができます。そのため、両方のコアが常に同じアドレスにアクセスする場合、それらはアドレスを争っているので、常にキャッシュミスになります。


4
+1、良い実用的なアドバイス。1つの追加:時間の局所性と空間の局所性の組み合わせは、たとえば行列演算の場合、それらをキャッシュラインに完全に適合するか、行/列がキャッシュラインに適合する小さい行列に分割することをお勧めします。マルチディムの視覚化のためにそれをしたことを覚えています。データ。それはズボンにいくつかの深刻なキックを提供しました。キャッシュは複数の「行」を保持することを覚えておくのは良いことです;)
AndreasT 2009

1
一度に1つのCPUだけがL1キャッシュ内の特定のアドレスを持つことができると言います-私はあなたがアドレスではなくキャッシュラインを意味していると思います。また、CPUの少なくとも1つが書き込みを行っているときに誤った共有の問題が発生することを聞いたことがありますが、両方が読み取りのみを行っている場合はそうではありません。「アクセス」とは実際に書き込みを意味するのですか?
ジョセフガービン

2
@JosephGarvin:はい、書き込みを意味しました。そうです、複数のコアが同時にL1キャッシュに同じキャッシュラインを持つことができますが、1つのコアがこれらのアドレスに書き込むと、他のすべてのL1キャッシュで無効になり、実行する前に再ロードする必要がありますそれで何でも。不正確な(間違った)表現でごめんなさい。:)
jalf

44

メモリとソフトウェアの相互作用に興味がある場合は、9部構成の記事、Ulrich Drepperによるすべてのプログラマがメモリについて知っておくべきことをお勧めします。104ページのPDFとしても入手できます。

この質問に特に関連するセクションは、パート2(CPUキャッシュ)とパート5(プログラマーができること-キャッシュの最適化)かもしれません。


16
記事の要点の要約を追加する必要があります。
Azmisov 14

素晴らしい読み物ですが、ここで言及しなければならないもう1つの本は、ヘネシー、パターソン、コンピュータアーキテクチャ、定量的アプローチです。
Haymo Kutschbach 2016年

15

データアクセスパターンとは別に、キャッシュに適したコードの主な要素はデータサイズです。データが少ないほど、より多くのデータがキャッシュに収まります。

これは主に、メモリ調整データ構造の要因です。「従来の」知識では、CPUは単語全体にしかアクセスできないため、データ構造は単語の境界で整列する必要があると述べており、単語に複数の値が含まれている場合は、追加の作業(単純な書き込みではなく読み取り-変更-書き込み)を行う必要があります。 。しかし、キャッシュはこの引数を完全に無効にすることができます。

同様に、Javaブール配列は、個々の値を直接操作できるようにするために、各値にバイト全体を使用します。実際のビットを使用すると、データサイズを8分の1に減らすことができますが、個々の値へのアクセスははるかに複雑になり、ビットシフトとマスク操作が必要になります(BitSetクラスがこれを行います)。ただし、キャッシュの影響により、配列が大きい場合は、boolean []を使用するよりもかなり高速になります。IIRC Iはかつてこの方法で2または3倍のスピードアップを達成しました。


9

キャッシュに最も効果的なデータ構造は配列です。CPUがメインメモリから一度にキャッシュライン全体(通常は32バイト以上)を読み取るときにデータ構造が順次配置される場合、キャッシュは最適に機能します。

ランダムな順序でメモリにアクセスするアルゴリズムは、ランダムにアクセスされたメモリに対応するために常に新しいキャッシュラインを必要とするため、キャッシュを破壊します。一方、配列を順番に実行するアルゴリズムは、次の理由で最適です。

  1. これは、CPUに先読みの機会を与えます。たとえば、後でより多くのメモリをキャッシュに投機的に投入します。この先読みにより、パフォーマンスが大幅に向上します。

  2. 大規模な配列でタイトなループを実行すると、CPUはループで実行されているコードをキャッシュでき、ほとんどの場合、外部メモリアクセスをブロックせずに、キャッシュメモリから完全にアルゴリズムを実行できます。


@Grover:あなたのポイントについて2.タイトなループの内側にある場合、ループカウントごとに関数が呼び出されている場合、新しいコードを完全にフェッチしてキャッシュミスを引き起こし、代わりにuが関数をforループ自体のコード、関数呼び出しはありません。キャッシュミスが少ないため、高速になりますか?
goldenmean 2009

1
はいといいえ。新しい関数がキャッシュに読み込まれます。十分なキャッシュスペースがある場合は、2回目の反復ですでにその関数がキャッシュにあるため、再度ロードする必要はありません。したがって、最初の呼び出しでヒットになります。C / C ++では、適切なセグメントを使用して関数を互いに隣接して配置するようコンパイラーに要求できます。
グローバー、

もう1つ注意:ループの外で呼び出したときに十分なキャッシュスペースがない場合は、新しい関数がキャッシュに読み込まれます。元のループがキャッシュからスローされることさえあります。この場合、呼び出しには反復ごとに最大3つのペナルティが発生します。1つは呼び出しターゲットをロードし、もう1つはループを再ロードします。ループヘッドがコールリターンアドレスと同じキャッシュラインにない場合は3番目。その場合、ループヘッドにジャンプするには、新しいメモリアクセスも必要です。
グローバー

8

ゲームエンジンで使用した例の1つは、データをオブジェクトから独自の配列に移動することでした。物理演算の対象となったゲームオブジェクトには、他にも多くのデータが添付されている場合があります。しかし、物理の更新ループ中は、位置、速度、質量、バウンディングボックスなどに関するデータがすべてのエンジンで処理されました。そのため、これらすべてが独自の配列に配置され、SSEに対して可能な限り最適化されました。

そのため、物理ループの間、物理演算データはベクトル演算を使用して配列順に処理されました。ゲームオブジェクトは、オブジェクトIDをさまざまな配列へのインデックスとして使用しました。配列を再配置する必要がある場合、ポインターが無効になる可能性があるため、これはポインターではありませんでした。

多くの点で、これはオブジェクト指向の設計パターンに違反しましたが、同じループで操作する必要があるデータを近接して配置することにより、コードを大幅に高速化しました。

最近のほとんどのゲームはHavokのようなビルド済みの物理エンジンを使用することを期待しているため、この例はおそらく時代遅れです。


2
+1まったく古くない。これは、ゲームエンジンのデータを整理する最良の方法です。データブロックを隣接させ、キャッシュの近接性/局所性を活用するために、次の(物理など)に進む前に、特定のタイプの操作(AIなど)をすべて実行します。参照。
エンジニア、

私はこの正確な例を数週間前のどこかでビデオで見ましたが、それへのリンクを失ってしまった/それを見つける方法を思い出せません。この例をどこで見たか覚えていますか?
ます

@意志:いいえ、これがどこにあったのか正確には覚えていません。
Zan Lynx

これがエンティティコンポーネントシステムの概念そのものです(ECS:en.wikipedia.org/wiki/Entity_component_system)。OOPプラクティスが推奨する従来の構造体配列ではなく、配列体としてデータを保存します。
BuschnicK

7

触れられた投稿は1つだけですが、プロセス間でデータを共有するときに大きな問題が発生します。複数のプロセスが同じキャッシュラインを同時に変更しようとしないようにする必要があります。ここで注意しなければならないのは「偽の」共有です。2つの隣接するデータ構造がキャッシュラインを共有し、一方を変更すると他方のキャッシュラインが無効になります。これにより、マルチプロセッサシステムでデータを共有するプロセッサキャッシュ間で、キャッシュラインが不必要に前後に移動する可能性があります。これを回避する方法は、データ構造をアラインしてパディングして別の行に配置することです。


7

ユーザー1800による「クラシックな例」への注釈へのコメント(コメントには長すぎます)

2つの反復順序(「外側」と「内側」)の時間差を確認したかったので、大きな2D配列で簡単な実験を行いました。

measure::start();
for ( int y = 0; y < N; ++y )
for ( int x = 0; x < N; ++x )
    sum += A[ x + y*N ];
measure::stop();

との2番目のケース forループが交換され。

遅いバージョン( "xファースト")は0.88秒で、速いバージョンは0.06秒でした。それがキャッシングの力です:)

私は使用gcc -O2しましたが、ループは最適化されていません。「最近のコンパイラーのほとんどはこれで自分で理解できる」というリカルドのコメントは保持されていません


わかりません。どちらの例でも、forループの各変数にアクセスしています。なぜ一方が他方よりも速いのですか?
ed- 2015年

最終的にはそれがどのように影響するかを理解するのに直感的:)
Laie

@EdwardCorlewアクセスされる順序が原因です。yの最初の順序は、データに順次アクセスするため、より高速です。最初のエントリが要求されると、L1キャッシュはキャッシュライン全体をロードします。これには、要求されたintに次の15が含まれる(64バイトのキャッシュラインを想定)ため、次の15を待つCPUストールはありません。 -アクセスされた要素がシーケンシャルではないため、最初の順序が遅くなり、おそらくNは十分に大きいため、アクセスされるメモリは常にL1キャッシュの外側にあり、すべての操作が停止します。
Matt Parkins、2016年

4

私は(2)と答えることができます。C++の世界では、リンクされたリストは簡単にCPUキャッシュを殺すことができるということです。可能であれば、配列の方が優れたソリューションです。同じことが他の言語にも当てはまるかどうかの経験はありませんが、同じ問題が発生することは容易に想像できます。


@Andrew:構造についてはどうですか。キャッシュは効率的ですか?キャッシュを効率的にするためのサイズ制限はありますか?
goldenmean

構造体はメモリの単一ブロックなので、キャッシュのサイズを超えない限り、影響はありません。構造体(またはクラス)のコレクションがある場合にのみ、キャッシュヒットが表示されます。これは、コレクションの構成方法によって異なります。配列はオブジェクトを互いに突き合わせますが(良好)、リンクリストにはアドレス空間全体にオブジェクトがあり、それらの間にリンクがあります。これは明らかにキャッシュパフォーマンスに悪影響を及ぼします。
Andrew

キャッシュを殺さずにリンクリストを使用するいくつかの方法は、大きなリストではない場合に最も効果的です。つまり、独自のメモリプールを作成することです。つまり、1つの大きな配列を割り当てます。次に、メモリ内のまったく異なる場所に割り当てられる可能性のある小さなリンクリストメンバーごとにメモリを「malloc」(またはC ++では「新規」)する代わりに、メモリ領域からメモリを割り当てます。リストのメンバーを論理的に閉じる確率が非常に高くなると、一緒にキャッシュに入れられます。
リラン・オレビ2009

もちろん、std :: list <>などを取得するのは大変な作業です。カスタムメモリブロックを使用します。私が若いホイッパースナッパーだったとき、私は絶対にその道を進みますが、最近は...他に取り組むべきことが多すぎます。
Andrew


4

キャッシュは「キャッシュライン」に配置され、(実)メモリはこのサイズのチャンクで読み書きされます。

したがって、単一のキャッシュラインに含まれるデータ構造は、より効率的です。

同様に、連続したメモリブロックにアクセスするアルゴリズムは、ランダムな順序でメモリをジャンプするアルゴリズムよりも効率的です。

残念ながら、キャッシュラインのサイズはプロセッサー間で劇的に異なるため、1つのプロセッサーで最適なデータ構造が他のプロセッサーでも効率的であることを保証する方法はありません。


必ずしも。偽りの共有に注意してください。場合によっては、データを異なるキャッシュラインに分割する必要があります。キャッシュの効果は、キャッシュの使用方法に常に依存しています。
DAG 2016年

4

コードを作成する方法を尋ねる、効果的なキャッシュに適したキャッシュ、およびその他のほとんどの質問は、通常、プログラムを最適化する方法を尋ねることです。これは、キャッシュがパフォーマンスに非常に大きな影響を与えるため、最適化されたプログラムはどれもキャッシュであるということです効果的なキャッシュフレンドリー。

最適化について読むことをお勧めします。このサイトにはいくつかの良い答えがあります。書籍に関しては、コンピュータシステム:プログラマの視点でお勧めします、キャッシュの適切な使用法について細かい説明があるます。

(ところで-キャッシュミスと同じくらいひどい場合は、さらに悪いことです-プログラムがハードドライブからページングしている場合...)


4

データ構造の選択、アクセスパターンなどの一般的なアドバイスについては多くの回答がありました。ここで、アクティブキャッシュ管理を利用するソフトウェアパイプラインと呼ばれる別のコード設計パターンを追加したいと思います。

このアイデアは、CPU命令のパイプライン処理など、他のパイプライン処理手法から借用したものです。

このタイプのパターンは、

  1. 合理的な複数のサブステップ、S [1]、S [2]、S [3]などに分解できます。その実行時間は、RAMアクセス時間(約60〜70ns)とほぼ同等です。
  2. 入力のバッチを取り、前述の複数のステップを実行して結果を取得します。

サブプロシージャが1つしかない単純なケースを考えてみましょう。通常、コードは次のようにします。

def proc(input):
    return sub-step(input))

パフォーマンスを向上させるには、複数の入力を関数にバッチで渡して、関数呼び出しのオーバーヘッドを償却し、コードキャッシュの局所性を高めることもできます。

def batch_proc(inputs):
    results = []
    for i in inputs:
        // avoids code cache miss, but still suffer data(inputs) miss
        results.append(sub-step(i))
    return res

ただし、前述のように、ステップの実行がRAMアクセス時間とほぼ同じ場合は、コードをさらに次のように改善できます。

def batch_pipelined_proc(inputs):
    for i in range(0, len(inputs)-1):
        prefetch(inputs[i+1])
        # work on current item while [i+1] is flying back from RAM
        results.append(sub-step(inputs[i-1]))

    results.append(sub-step(inputs[-1]))

実行フローは次のようになります。

  1. prefetch(1)は 、CPUにinput [1]をキャッシュにプリフェッチするように要求します。プリフェッチ命令は、Pサイクルで戻り、バックグラウンドで、input [1]はRサイクル後にキャッシュに到着します。
  2. works_on(0)は0で コールドミスして動作します。
  3. prefetch(2)は 別のフェッチを発行します
  4. works_on(1) P + R <= Mの場合、inputs [1]はこのステップの前にすでにキャッシュにある必要があるため、データキャッシュミスを回避します
  5. works_on(2) ...

より多くのステップが含まれる可能性がある場合は、ステップとメモリアクセスのレイテンシが一致する限り、マルチステージパイプラインを設計できます。コード/データキャッシュミスはほとんど発生しません。ただし、このプロセスは、ステップとプリフェッチ時間の正しいグループを見つけるために、多くの実験で調整する必要があります。その必要な努力により、高性能データ/パケットストリーム処理への採用が増えています。優れた製品コードの例は、DPDK QoSエンキューパイプラインの設計にあります。http://dpdk.org/doc/guides/prog_guide/qos_framework.html //dpdk.org/doc/guides/prog_guide/qos_framework.html Chapter 21.2.4.3。エンキューパイプライン。

詳細情報を見つけることができます:

https://software.intel.com/en-us/articles/memory-management-for-optimal-performance-on-intel-xeon-phi-coprocessor-alignment-and

http://infolab.stanford.edu/~ullman/dragon/w06/lectures/cs243-lec13-wei.pdf


1

最小サイズになるようにプログラムを作成します。そのため、GCCに-O3最適化を使用することが常に良いとは限りません。大きいサイズになります。多くの場合、-Osは-O2と同じくらい優れています。ただし、使用するプロセッサによって異なります。YMMV。

小さなデータのチャンクを一度に処理します。データセットが大きい場合、効率の悪いソートアルゴリズムがクイックソートよりも速く実行できるのはそのためです。大きなデータセットを小さなデータセットに分割する方法を見つけます。他の人がこれを提案しています。

命令の時間的/空間的局所性をより有効に活用できるようにするために、コードがアセンブリに変換される方法を調査する必要がある場合があります。例えば:

for(i = 0; i < MAX; ++i)
for(i = MAX; i > 0; --i)

2つのループは、配列を解析するだけの場合でも、異なるコードを生成します。いずれにせよ、あなたの質問はアーキテクチャ固有のものです。したがって、キャッシュの使用を厳密に制御する唯一の方法は、ハードウェアの動作を理解し、それに合わせてコードを最適化することです。


興味深い点。先読みキャッシュは、メモリのループ/パスの方向に基づいて仮定を行いますか?
アンドリュー、

1
投機的データキャッシュを設計する方法はたくさんあります。ストライドベースのものは、データアクセスの「距離」と「方向」を測定します。コンテンツベースのものは、ポインタチェーンを追跡します。それらを設計する他の方法があります。
sybreon 2009

1

構造体とフィールドの整列に加えて、構造体にヒープが割り当てられている場合は、整列された割り当てをサポートするアロケーターを使用できます。_aligned_malloc(sizeof(DATA)、SYSTEM_CACHE_LINE_SIZE); そうしないと、ランダムな偽りの共有が発生する可能性があります。Windowsでは、デフォルトのヒープには16バイトの境界整列があることに注意してください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.