Ulrich Drepperの論文で、すべてのプログラマーがメモリについて知っておくべきこと、第3部:CPUキャッシュ、彼は「ワーキングセット」サイズと操作ごとに消費するCPUサイクル(この場合は順次読み取り)の関係を示すグラフを示しています。また、グラフには、L1キャッシュとL2キャッシュのサイズを示す2つのジャンプがあります。cで効果を再現するために独自のプログラムを作成しました。int []配列を頭から尾まで順番に読み取るだけで、さまざまなサイズの配列(1KBから1MB)を試しました。データをグラフにプロットしましたが、ジャンプはなく、グラフは直線です。
私の質問は次のとおりです。
- 私の方法に何か問題がありますか?CPUキャッシュ効果を生成する正しい方法は何ですか(ジャンプを確認するため)。
- シーケンシャル読み取りの場合、次のように動作するはずだと思っていました。最初の要素を読み取ると、キャッシュミスであり、キャッシュラインサイズ(64K)内でヒットが発生します。プリフェッチの助けを借りて、次のキャッシュラインを読み取るレイテンシーは隠されます。ワーキングセットのサイズがL1キャッシュのサイズを超えている場合でも、データを連続してL1キャッシュに読み込み、最も使用頻度の低いものを削除して、プリフェッチを続行します。したがって、キャッシュミスのほとんどは非表示になり、L2からのデータのフェッチにかかる時間は読み取りアクティビティの背後に非表示になります。つまり、同時に動作しているということです。連想性(私の場合は8ウェイ)は、L2からデータを読み取るレイテンシーを隠します。だから、私のプログラムの現象は正しいはずです、私は何かが欠けていますか?
- Javaで同じ効果を得ることができますか?
ちなみに、私はこれをLinuxで行っています。
編集1
Stephen Cの提案に感謝します、ここにいくつかの追加情報があります:これは私のコードです:
int *arrayInt;
void initInt(long len) {
int i;
arrayInt = (int *)malloc(len * sizeof(int));
memset(arrayInt, 0, len * sizeof(int));
}
long sreadInt(long len) {
int sum = 0;
struct timespec tsStart, tsEnd;
initInt(len);
clock_gettime(CLOCK_REALTIME, &tsStart);
for(i = 0; i < len; i++) {
sum += arrayInt[i];
}
clock_gettime(CLOCK_REALTIME, &tsEnd);
free(arrayInt);
return (tsEnd.tv_nsec - tsStart.tv_nsec) / len;
}
main()関数では、配列サイズの1KBから100MBを試しましたが、それでも同じですが、要素ごとにかかる平均時間は2ナノ秒です。時間はL1dのアクセス時間だと思います。
私のキャッシュサイズ:
L1d == 32k
L2 == 256k
L3 == 6144k
編集2
リンクリストを使用するようにコードを変更しました。
// element type
struct l {
struct l *n;
long int pad[NPAD]; // the NPAD could be changed, in my case I set it to 1
};
struct l *array;
long globalSum;
// for init the array
void init(long len) {
long i, j;
struct l *ptr;
array = (struct l*)malloc(sizeof(struct l));
ptr = array;
for(j = 0; j < NPAD; j++) {
ptr->pad[j] = j;
}
ptr->n = NULL;
for(i = 1; i < len; i++) {
ptr->n = (struct l*)malloc(sizeof(struct l));
ptr = ptr->n;
for(j = 0; j < NPAD; j++) {
ptr->pad[j] = i + j;
}
ptr->n = NULL;
}
}
// for free the array when operation is done
void release() {
struct l *ptr = array;
struct l *tmp = NULL;
while(ptr) {
tmp = ptr;
ptr = ptr->n;
free(tmp);
}
}
double sread(long len) {
int i;
long sum = 0;
struct l *ptr;
struct timespec tsStart, tsEnd;
init(len);
ptr = array;
clock_gettime(CLOCK_REALTIME, &tsStart);
while(ptr) {
for(i = 0; i < NPAD; i++) {
sum += ptr->pad[i];
}
ptr = ptr->n;
}
clock_gettime(CLOCK_REALTIME, &tsEnd);
release();
globalSum += sum;
return (double)(tsEnd.tv_nsec - tsStart.tv_nsec) / (double)len;
}
最後に、コンパイラの最適化を回避するために、globalSumをprintfします。ご覧のとおり、これはまだ順次読み取りです。最大500MBの配列サイズを試しましたが、要素あたりの平均時間は約4ナノ秒です(データ「パッド」とポインター「にアクセスする必要があるためかもしれません)」 n'、2回のアクセス)、アレイサイズの1KBと同じ。ですから、プリフェッチのようなキャッシュの最適化がレイテンシーを非常にうまく隠しているからだと思いますよね?ランダムアクセスを試して、後で結果を表示します。
編集3
リンクリストへのランダムアクセスを試しましたが、結果は次のとおりです。
最初の赤い線は私のL1キャッシュサイズ、2番目はL2です。だから私たちはそこに少しジャンプするのを見ることができます。また、レイテンシーがまだうまく隠されている場合もあります。