少なくとも一般的なデスクトップCPUでは、キャッシュの使用量について直接指定することはできません。ただし、キャッシュに適したコードを作成することはできます。コード側では、これは多くの場合、ループの展開(1つの明白な例)がほとんど役に立たないことを意味します-それはコードを拡張し、最新のCPUは通常ループのオーバーヘッドを最小限に抑えます。通常、データ側でより多くのことを実行して、参照の局所性を改善し、偽共有から保護することができます(たとえば、キャッシュの同じ部分を使用しようとし、他の部分は未使用のままにする2つの頻繁に使用されるデータ)。
編集(いくつかのポイントをもう少し明確にするため):
一般的なCPUには、さまざまなキャッシュがあります。最新のデスクトッププロセッサには、通常、少なくとも2レベル、多くの場合3レベルのキャッシュがあります。(少なくともほぼ)普遍的な合意により、「レベル1」は処理要素に「最も近い」キャッシュであり、そこから数値が増加します(レベル2が次、レベル3がその後など)。
ほとんどの場合、(少なくとも)レベル1キャッシュは、命令キャッシュとデータキャッシュの2つに分割されます(Intel 486は、私が知っているほぼ唯一の例外であり、命令とデータの両方に1つのキャッシュがあります) -しかし、それは完全に時代遅れであり、おそらく多くの考えに値するものではありません)。
ほとんどの場合、キャッシュは一連の「行」として編成されます。キャッシュの内容は通常、一度に1行ずつ読み取られ、書き込まれ、追跡されます。つまり、CPUがキャッシュラインのいずれかの部分からのデータを使用する場合、そのキャッシュライン全体が次に低いレベルのストレージから読み取られます。CPUに近いキャッシュは、一般的に小さく、キャッシュラインも小さくなります。
この基本的なアーキテクチャは、コードの記述で重要なキャッシュの特性のほとんどにつながります。可能な限り、何かを一度キャッシュに読み込んで、それを使ってすべてを実行してから、別の何かに移りたいと考えています。
つまり、データを処理しているときは、通常、比較的少量のデータ(キャッシュに収まるほど少ない)を読み取り、そのデータに対してできるだけ多くの処理を行ってから、次のチャンクに移動する方がよいということです。データ。大量の入力を徐々に小さな部分にすばやく分割するクイックソートのようなアルゴリズムは、これを多かれ少なかれ自動的に実行するため、キャッシュの正確な詳細にほとんど関係なく、かなりキャッシュフレンドリーになる傾向があります。
これは、コードの記述方法にも影響を及ぼします。次のようなループがある場合:
for i = 0 to whatever
step1(data);
step2(data);
step3(data);
end for
一般に、キャッシュに収まる量まで、できるだけ多くのステップをつなぎ合わせる方がよいでしょう。キャッシュをオーバーフローさせた瞬間に、パフォーマンスが大幅に低下する可能性があります。上記の手順3のコードが十分に大きく、キャッシュに収まらない場合は、通常、ループを次のように2つに分割することをお勧めします(可能な場合)。
for i = 0 to whatever
step1(data);
step2(data);
end for
for i = 0 to whatever
step3(data);
end for
ループ展開はかなり熱く争われている主題です。一方では、CPUにはるかに適したコードにつながる可能性があり、ループ自体に対して実行される命令のオーバーヘッドが削減されます。同時に、コードサイズを増やすことができる(そして一般的にはそうする)ので、比較的キャッシュに不向きです。私自身の経験では、非常に大量のデータに対して非常に少量の処理を行う傾向がある合成ベンチマークでは、ループ展開から多くの利益を得ることができます。個々のデータに対してより多くの処理を行う傾向があるより実用的なコードでは、取得する量ははるかに少なくなります。また、キャッシュがオーバーフローして深刻なパフォーマンスの低下が発生することは、特にまれではありません。
データキャッシュのサイズも制限されています。これは、通常、できるだけ多くのデータがキャッシュに収まるように、データをできるだけ密にパックする必要があることを意味します。明らかな例の1つとして、ポインターとリンクされたデータ構造は、それらのポインターによって使用されるデータキャッシュスペースの量を補うために、計算の複雑さの点でかなりの量を獲得する必要があります。リンクされたデータ構造を使用する場合は、通常、少なくとも比較的大きなデータをリンクしていることを確認する必要があります。
ただし、多くの場合、数十年にわたって(ほとんど)廃止されてきた小さなプロセッサのごく少量のメモリにデータを収めるために最初に学んだトリックは、最新のプロセッサではかなりうまく機能することがわかりました。現在の目的は、メインメモリではなくキャッシュにより多くのデータを収めることですが、効果はほぼ同じです。かなりの数のケースで、CPU命令はほぼ無料であると考えることができ、全体的な実行速度はキャッシュ(またはメインメモリ)への帯域幅によって制御されるため、高密度フォーマットからデータを解凍するための追加の処理は、あなたの好意。これは、すべてがキャッシュに収まらないほど十分なデータを処理している場合に特に当てはまります。そのため、全体的な速度はメインメモリへの帯域幅によって決まります。この場合、あなたはたくさん実行することができますいくつかのメモリ読み取りを節約し、それでも先に出てくるように指示します。
並列処理はその問題を悪化させる可能性があります。多くの場合、並列処理を可能にするためにコードを書き直すと、パフォーマンスが実質的に向上しないか、場合によってはパフォーマンスが低下する可能性があります。全体的な速度がCPUからメモリまでの帯域幅によって支配されている場合、その帯域幅をめぐってより多くのコアが競合することは、何の役にも立たない可能性があります(そして実質的な害を及ぼす可能性があります)。このような場合、速度を向上させるために複数のコアを使用することは、多くの場合、データをより緊密にパックするためにさらに多くのことを行い、データをアンパックするためにさらに多くの処理能力を利用することになります。したがって、実際の速度の向上は、消費される帯域幅の削減によるものです。 、および追加のコアは、より高密度の形式からデータを解凍するために時間を無駄にすることを防ぎます。
並列コーディングで発生する可能性のあるもう1つのキャッシュベースの問題は、変数の共有(および偽共有)です。2つ(またはそれ以上)のコアがメモリ内の同じ場所に書き込む必要がある場合、そのデータを保持するキャッシュラインは、コア間を行き来して、各コアに共有データへのアクセスを許可することになります。その結果、多くの場合、コードはシリアルよりもパラレルで実行速度が遅くなります(つまり、シングルコアで実行されます)。これには「偽共有」と呼ばれるバリエーションがあり、異なるコアのコードが別々のデータに書き込んでいますが、異なるコアのデータは同じキャッシュラインに配置されます。キャッシュはデータの行全体に関して純粋にデータを制御するため、データはとにかくコア間でシャッフルされ、まったく同じ問題が発生します。