8

Ulrich Drepperの論文で、すべてのプログラマーがメモリについて知っておくべきこと、第3部:CPUキャッシュ、彼は「ワーキングセット」サイズと操作ごとに消費するCPUサイクル(この場合は順次読み取り)の関係を示すグラフを示しています。また、グラフには、L1キャッシュとL2キャッシュのサイズを示す2つのジャンプがあります。cで効果を再現するために独自のプログラムを作成しました。int []配列を頭から尾まで順番に読み取るだけで、さまざまなサイズの配列(1KBから1MB)を試しました。データをグラフにプロットしましたが、ジャンプはなく、グラフは直線です。

私の質問は次のとおりです。

  1. 私の方法に何か問題がありますか?CPUキャッシュ効果を生成する正しい方法は何ですか(ジャンプを確認するため)。
  2. シーケンシャル読み取りの場合、次のように動作するはずだと思っていました。最初の要素を読み取ると、キャッシュミスであり、キャッシュラインサイズ(64K)内でヒットが発生します。プリフェッチの助けを借りて、次のキャッシュラインを読み取るレイテンシーは隠されます。ワーキングセットのサイズがL1キャッシュのサイズを超えている場合でも、データを連続してL1キャッシュに読み込み、最も使用頻度の低いものを削除して、プリフェッチを続行します。したがって、キャッシュミスのほとんどは非表示になり、L2からのデータのフェッチにかかる時間は読み取りアクティビティの背後に非表示になります。つまり、同時に動作しているということです。連想性(私の場合は8ウェイ)は、L2からデータを読み取るレイテンシーを隠します。だから、私のプログラムの現象は正しいはずです、私は何かが欠けていますか?
  3. 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です。だから私たちはそこに少しジャンプするのを見ることができます。また、レイテンシーがまだうまく隠されている場合もあります。

4

5 に答える 5

5

この答えは答えではありませんが、一連のメモの詳細です。

まず、CPUは、個々のバイト/ワード/ dwordではなく、キャッシュラインで動作する傾向があります。つまり、整数の配列を順番に読み取り/書き込みする場合、キャッシュラインへの最初のアクセスによってキャッシュミスが発生する可能性がありますが、同じキャッシュライン内の異なる整数への後続のアクセスでは発生しません。64バイトのキャッシュラインと4バイトの整数の場合、これは、16回のアクセスごとに1回だけキャッシュミスが発生することを意味します。結果を薄めるでしょう。

次に、CPUには「ハードウェアプリフェッチャー」があります。キャッシュラインが順番に読み取られていることを検出すると、ハードウェアプリフェッチャーは、次に必要になると予測したキャッシュラインを自動的にプリフェッチします(必要になる前にキャッシュラインにフェッチしようとします)。

第三に、CPUはフェッチコストを隠すために他のこと(「アウトオブオーダー実行」など)を行います。測定できる時間差(キャッシュヒットとキャッシュミスの間)は、CPUが隠れることができなかった時間であり、フェッチの総コストではありません。

これらの3つのことを組み合わせると、次のようになります。整数の配列を順番に読み取る場合、前のキャッシュラインから16回の読み取りを実行しているときに、CPUが次のキャッシュラインをプリフェッチする可能性があります。また、キャッシュミスのコストは目立たず、完全に隠されている可能性があります。これを防ぐために; 「ワーキングセットがキャッシュに収まる」と「ワーキングセットがキャッシュに収まらない」の間で測定されるパフォーマンスの差を最大化するために、各キャッシュラインに「ランダムに」アクセスする必要があります。

最後に、測定に影響を与える可能性のある他の要因があります。たとえば、ページングを使用するOS(Linuxやその他のほとんどすべての最新のOS)の場合、これらすべての上にキャッシュのレイヤー全体があり(TLB /トランスレーションルックアサイドバッファー)、ワーキングセットが特定のサイズを超えるとTLBは失敗します; これは、グラフの4番目の「ステップ」として表示されるはずです。カーネルからの干渉もあります(IRQ、ページフォールト、タスクスイッチ、複数のCPUなど)。これは、グラフにランダムな静的/エラーとして表示される場合があります(テストが頻繁に繰り返され、外れ値が破棄されない限り)。カーネルによって割り当てられた物理アドレスに依存する方法でキャッシュの有効性を低下させる可能性のあるキャッシュ設計のアーティファクト(キャッシュの関連付け)もあります。これは「ステップ」と見なされる可能性があります

于 2012-09-23T13:52:43.947 に答える
3

私の方法に何か問題がありますか?

おそらく、しかし答えられないあなたの実際のコードを見ずに。

  • コードが何をしているのかについての説明は、配列を1回読んでいるのか何度も読んでいるのかを示していません。

  • ハードウェアによっては、アレイのサイズが十分でない場合があります。(最近のチップの中には、数メガバイトの第3レベルのキャッシュがないものがありますか?)

  • 特にJavaの場合、意味のあるマイクロベンチマークを実装するには、正しい方法で多くのことを行う必要があります。


Cの場合:

  • Cコンパイラの最適化スイッチを調整してみてください。

  • コードは配列にシリアルにアクセスしているため、コンパイラはCPUが追いつくことができるように命令を順序付けることができるか、CPUが楽観的にプリフェッチまたはワイドフェッチを実行している可能性があります。予測しにくい順序で配列要素を読み取ってみることができます。

  • ループ計算の結果は何にも使用されないため、コンパイラがループを完全に最適化した可能性もあります。

(このQ&Aによると、メモリから1ワードをフェッチするのにどのくらいの時間がかかりますか?、L2キャッシュからのフェッチは約7ナノ秒、メインメモリからのフェッチは約100ナノ秒です。しかし、最大2ナノ秒になります。何か賢いです。観察しているのと同じ速さで実行するには、ここで実行する必要があります。)

于 2012-09-23T00:57:52.943 に答える
1

gcc-4.7とコンパイルを使用gcc -std=c99 -O2 -S -D_GNU_SOURCE -fverbose-asm tcache.cすると、コンパイラがforループを削除するのに十分な最適化を行っていることがわかります(sum使用されていないため)。

私はあなたのソースコードを改善しなければなりませんでした。一部の#include-sが欠落iしており、2番目の関数で宣言されていないため、例はそのままではコンパイルされません。

グローバルsum変数を作成するか、それを何らかの方法で呼び出し元に渡します(おそらく、グローバル変数を使用しint globalsum;globalsum=sum;ループの後に配置します)。

そして、あなたが。で配列をクリアするのが正しいかどうかはわかりませんmemset。私はあなたがすべてゼロを合計していることを理解している賢い十分なコンパイラを想像することができました。

最後に、コードは非常に規則的な動作をし、局所性が良好です。ときどきキャッシュミスが発生し、キャッシュライン全体が読み込まれ、データは多くの反復に十分対応できます。いくつかの巧妙な最適化(例えば-O3、それ以上)は良いprefetch指示を生成するかもしれません。これはキャッシュに最適です。32ワードのL1キャッシュラインの場合、キャッシュミスは32ループごとに発生するため、十分に償却されます。

データのリンクリストを作成すると、キャッシュの動作が悪化します。逆に、実際のプログラムの中には、適切に選択されたいくつかの場所に__builtin_prefetchを注意深く追加すると、パフォーマンスが10%以上向上する場合があります(ただし、追加しすぎるとパフォーマンスが低下します)。

実際には、プロセッサはキャッシュを待機するために大部分の時間を費やしています(そして、それを測定することは困難です。この待機はCPU時間であり、アイドル時間ではありません)。L3キャッシュミスの間、RAMモジュールからデータをロードするのに必要な時間は、何百ものマシン命令を実行するのに必要な時間であることを忘れないでください。

于 2012-09-23T06:36:26.013 に答える
0

1と2についてははっきりとは言えませんが、Javaでこのようなテストを正常に実行することはより困難です。特に、自動ガベージコレクションなどの管理言語機能がテストの途中で発生し、結果が失われる可能性があるのではないかと心配するかもしれません。

于 2012-09-23T00:13:10.357 に答える
0

グラフ3.26からわかるように、Intel Core 2は、読み取り中にジャンプをほとんど示しません(グラフの上部にある赤い線)。ジャンプがはっきりと見えるところに書き込み/コピーしています。書き込みテストを行う方が良いです。

于 2012-09-23T06:30:45.917 に答える