SOに関するこの最近の質問と与えられた回答に触発されて、私は非常に無知であると感じました。私はCPUキャッシングについてもっと学ぶために時間を費やすことに決め、このすべてが正しく行われているかどうかを確認するための小さなプログラムを作成しました(ほとんどおそらくそうではないでしょう、私は恐れています)。私は最初に私の期待の根底にある仮定を書き留めます、それでそれらが間違っているならあなたはおそらくここで私を止めることができます。私が読んだものに基づいて、一般的に:
- -way
n
アソシアティブキャッシュはセットに分割されs
、各セットにはn
行が含まれ、各行のサイズは固定されていL
ます。 - 各メインメモリアドレスは、 1セットの任意のキャッシュラインに
A
マップできます。n
- アドレスがマップされるセット
A
は、アドレス空間を1つのキャッシュラインのサイズごとにスロットに分割し、A
のスロット(I = A / L
)のインデックスを計算し、最後にモジュロ演算を実行してインデックスをターゲットにマップすることで見つけることができます。セットT
(T = I % s
); - メインメモリラインがフェッチされるのを待っている間、CPUがストールしてアイドル状態を維持する可能性が低いため、キャッシュ読み取りミスはキャッシュ書き込みミスよりも高い遅延を引き起こします。
私の最初の質問は:これらの仮定は正しいですか?
それらがそうであると仮定して、私はこれらの概念を少し試してみたので、実際にそれらがプログラムに具体的な影響を与えるのを見ることができました。バイトのメモリバッファを割り当て、バッファの先頭から特定のステップの固定増分B
でそのバッファの場所に繰り返しアクセスする簡単なテストを作成しました(つまり、が14で、ステップが3の場合、場所0のみに繰り返しアクセスします。 、3、6、9、および12-が13、14、または15の場合も同様です): B
B
int index = 0;
for (int i = 0; i < REPS; i++)
{
index += STEP;
if (index >= B) { index = 0; }
buffer[index] = ...; // Do something here!
}
上記の仮定により、私の期待は次のとおりでした。
- クリティカルストライド
STEP
に等しく設定する場合(つまり、キャッシュラインのサイズにキャッシュ内のセット数を掛けたもの、または)、たとえば(、メモリのみにアクセスするため)に設定した場合よりもパフォーマンスが大幅に低下するはずです。同じセットにマップされる場所。キャッシュラインがそのセットからより頻繁に削除され、キャッシュミスの発生率が高くなります。L * s
STEP
L * s) + 1
STEP
がクリティカルストライドに等しい場合、これが小さすぎない限り、パフォーマンスはバッファのサイズに影響されないはずです(そうでない場合、アクセスされる場所が少なすぎて、キャッシュミスが少なくなります)。B
そうしないと、パフォーマンスが影響を受けるはずです。B
バッファが大きいほど、異なるセットにマップされる場所にアクセスする可能性が高くなります(特にSTEP
、2の倍数でない場合)。- 各バッファロケーションからの読み取りと各バッファロケーションへの書き込みの場合、それらのロケーションへの書き込みのみの場合よりもパフォーマンスの低下が悪化するはずです。メモリロケーションへの書き込みでは、対応する行がフェッチされるのを待つ必要がないため、同じセット(ここでも、クリティカルストライドを使用することによる
STEP
)は、小さな影響を与えるはずです。
そこで、RightMark Memory Analyzerを使用して、L1 CPUデータキャッシュのパラメーターを見つけ、プログラムのサイズを調整して、試してみました。これが私がメインサイクルを書いた方法です(onlyWriteToCache
コマンドラインから設定できるフラグです):
...
for (int i = 0; i < REPS; i++)
{
...
if (onlyWriteToCache)
{
buffer[index] = (char)(index % 255);
}
else
{
buffer[index] = (char)(buffer[index] % 255);
}
}
要するに結果:
- 期待1)と2)が確認されました。
- 期待3)は確認されなかった。
この事実は私を驚かせ、私が完全に正しくなかった何かがあると私に思わせます。B
が256MBでSTEP
、クリティカルストライドに等しい場合、テスト(GCC 4.7.1で-O3を使用してコンパイル)は次のことを示します。
- 書き込み専用バージョンのサイクルでは、平均で最大6倍のパフォーマンス低下が発生します(6.234秒対1.078秒)。
- サイクルの読み取り/書き込みバージョンでは、平均で約1.3倍のパフォーマンス低下が発生します(6.671秒対5.25秒)。
だから私の2番目の質問は:なぜこの違いがあるのですか?書き込みのみの場合よりも読み取りと書き込みの場合の方がパフォーマンスの低下が大きいと思います。
完全を期すために、テストを実行するために作成したプログラムを以下に示します。定数は、マシンのハードウェアパラメーターを反映しています。L18ウェイアソシアティブデータキャッシュのサイズは32 KBで、L
各キャッシュラインのサイズは次のとおりです。 64バイト。合計64セットになります(CPUには、同じサイズで同じラインサイズの個別のL1 8ウェイ命令キャッシュがあります)。
#include <iostream>
#include <ctime>
#include <cstdlib>
#include <iterator>
#include <algorithm>
using namespace std;
// Auxiliary functions
constexpr int pow(int base, int exp)
{
return ((exp == 0) ? 1 : base * pow(base, exp - 1));
}
int main(int argc, char* argv[])
{
//======================================================================
// Define behavior from command-line arguments
//======================================================================
bool useCriticalStep = false;
bool onlyWriteToCache = true;
size_t BUFFER_SIZE = pow(2, 28);
size_t REPS = pow(2, 27);
if (argc > 0)
{
for (int i = 1; i < argc; i++)
{
string option = argv[i];
if (option == "-c")
{
useCriticalStep = true;
}
else if (option == "-r")
{
onlyWriteToCache = false;
}
else if (option[1] == 's')
{
string encodedSizeInMB = option.substr(2);
size_t sizeInMB = atoi(encodedSizeInMB.c_str());
BUFFER_SIZE = sizeInMB * pow(2, 20);
}
else if (option[1] == 'f')
{
string encodedNumOfReps = option.substr(2);
size_t millionsOfReps = atoi(encodedNumOfReps.c_str());
REPS = millionsOfReps * pow(10, 6);
}
}
}
//======================================================================
// Machine parameters
//======================================================================
constexpr int CACHE_SIZE = pow(2, 15);
constexpr int CACHE_LINE_SIZE = 64;
constexpr int CACHE_LINES_PER_SET = 8;
constexpr int SET_SIZE = CACHE_LINE_SIZE * CACHE_LINES_PER_SET;
constexpr int NUM_OF_SETS = CACHE_SIZE / SET_SIZE;
//======================================================================
// Print out the machine parameters
//======================================================================
cout << "CACHE SIZE: " << CACHE_SIZE / 1024 << " KB" << endl;
cout << "CACHE LINE SIZE: " << CACHE_LINE_SIZE << " bytes" << endl;
cout << "CACHE LINES PER SET: " << CACHE_LINES_PER_SET << endl;
cout << "SET SIZE: " << SET_SIZE << " bytes" << endl;
cout << "NUMBER OF SETS: " << NUM_OF_SETS << endl;
fill_n(ostream_iterator<char>(cout), 30, '='); cout << endl;
//======================================================================
// Test parameters
//======================================================================
const int STEP = NUM_OF_SETS * CACHE_LINE_SIZE + (useCriticalStep ? 0 : 1);
//======================================================================
// Print out the machine parameters
//======================================================================
cout << "BUFFER SIZE: " << BUFFER_SIZE / pow(2, 20) << " MB" << endl;
cout << "STEP SIZE: " << STEP << " bytes" << endl;
cout << "NUMBER OF REPS: " << REPS << endl;
fill_n(ostream_iterator<char>(cout), 30, '='); cout << endl;
//======================================================================
// Start the test
//======================================================================
char* buffer = new char[BUFFER_SIZE];
clock_t t1 = clock();
int index = 0;
for (size_t i = 0; i < REPS; i++)
{
index += STEP;
if (index >= BUFFER_SIZE)
{
index = 0;
}
if (onlyWriteToCache)
{
buffer[index] = (char)(index % 255);
}
else
{
buffer[index] = (char)(buffer[index] % 255);
}
}
clock_t t2 = clock();
//======================================================================
// Print the execution time (in clock ticks) and cleanup resources
//======================================================================
float executionTime = (float)(t2 - t1) / CLOCKS_PER_SEC;
cout << "EXECUTION TIME: " << executionTime << "s" << endl;
delete[] buffer;
}
この長い質問をなんとか読んでくださった方、よろしくお願いします。