ランダム読み取りの並列化はうまくいくようです-なぜですか?


18

次の非常に単純なコンピュータープログラムを検討してください。

for i = 1 to n:
    y[i] = x[p[i]]

ここで、とは要素のバイト配列であり、は要素の単語配列です。ここで、は大きく、たとえば(したがって、データのごく一部のみがあらゆる種類のキャッシュメモリに収まります)。y n p n n n = 2 31xynpnnn=231

がから間に一様に分布した乱数で構成されていると仮定します。1 np1n

最新のハードウェアの観点から見ると、これは次のことを意味するはずです。

  • 読書安い(シーケンシャルリード)でありますp[i]
  • 読ん非常に高価である(ランダム読み取り、ほぼすべてのあるキャッシュミスを読み込みます。私たちは、メインメモリから個々のバイトを取得する必要があります)x[p[i]]
  • 書き込み安い(シーケンシャル書き込み)です。y[i]

そして、これはまさに私が観察していることです。このプログラムは、シーケンシャルな読み取りと書き込みのみを行うプログラムと比較して非常に遅いです。すごい。

ここで疑問が生じます。このプログラムは、最新のマルチコアプラットフォームでどの程度並列化されますか?


私の仮説では、このプログラムはうまく並列化されません。結局のところ、ボトルネックはメインメモリです。単一のコアは、メインメモリからのデータを待機するだけで、すでにほとんどの時間を無駄にしています。

ただし、これは、この種の操作がボトルネックとなっているいくつかのアルゴリズムを試し始めたときに観察したものではありませんでした!

単純なforループをOpenMP並列forループに置き換えました(本質的に、範囲を小さな部分に分割し、これらの部分を異なるCPUコアで並列に実行します)。[1,n]

ローエンドのコンピューターでは、スピードアップは確かに軽微でした。しかし、ハイエンドプラットフォームでは、優れた線形に近い高速化が得られたことに驚きました。いくつかの具体的な例(正確なタイミングは少しずれている可能性があり、多くのランダムな変動があります;これらは単なる簡単な実験でした):

  • 2 x 4コアXeon(合計8コア):シングルスレッドバージョンと比較して、5〜8倍高速化。

  • 2 x 6コアXeon(合計12コア):シングルスレッドバージョンと比較して8〜14倍高速化。

今、これは全く予想外でした。質問:

  1. 正確になぜプログラムの並列化のこの種のは、とてもよくありませんか?ハードウェアで何が起こりますか?(私の現在の推測は、これらの線に沿ったものです:異なるスレッドからのランダムな読み取りは「パイプライン化」されており、これらに対する回答を取得する平均レートは、単一のスレッドの場合よりもはるかに高くなります。)

  2. 速度を上げるには、複数のスレッドと複数のコアを使用する必要がありますか?メインメモリとCPUの間のインターフェイスで何らかのパイプライン処理が実際に行われる場合、シングルスレッドアプリケーションでは、メインメモリに、、...、コンピュータはメインメモリから関連するキャッシュラインのフェッチを開始できますか?これが原則的に可能である場合、実際にどのように達成しますか?x [ p [ i + 1 ] ]x[p[i]]x[p[i+1]]

  3. この種のプログラムの分析(およびパフォーマンスの正しい予測)に使用できる正しい理論モデルは何ですか?


編集:https : //github.com/suomela/parallel-random-readからソースコードとベンチマーク結果を入手できます。

球場の数字の例():n=232

  • 約 単一スレッドで反復あたり42 ns(ランダム読み取り)
  • 約 12コアで反復あたり5 ns(ランダム読み取り)。

回答:


9

メインメモリおよびレベル3キャッシュへのアクセスに関連する問題をすべて忘れてください。並列の観点から、これらの問題を無視して、プロセッサー(またはコア)を使用する場合、プログラムは完全に並列化します。ドメイン分解によって行われる作業を分割すると、各コアはまたは要素。機能的な依存関係がないため、通信や同期のオーバーヘッドはありません。プロセッサ。したがって、メモリの問題を無視すると、等しい高速化が期待されます。n個pn個npPnpp

次に、メモリの問題を考慮しましょう。ハイエンドXeonベースのノードで実際に観察した超線形の高速化は、次のように正当化されます。

並列システムは、メモリが階層的であり、プログラムが使用するメモリでアクセス時間が(個別のステップで)増加する場合に、このような動作を示す可能性があります。この場合、有効な計算速度は、同様のプロセッサーを使用する並列コンピューターよりもシリアルプロセッサーの方が遅くなる可能性があります。これは、バイトのメモリを使用するシーケンシャルアルゴリズムは、プロセッサパラレルシステムの各プロセッサでバイトのみを使用するのに対し、キャッシュと仮想メモリの影響により、シリアルプロセッサの有効な計算速度が低下する可能性があるためです。n / p pnn/pp

以下のためにバイト、我々は、メモリ2048バイトを必要とします。ただし、最後の例のように12個のコアを使用する場合、すべてのコアで処理できるデータは2048/12 Mバイト(約170 Mバイト)だけです。ハイエンドXeonプロセッサには、キャッシュレベル3が装備されており、そのサイズは15〜30 Mバイトです。明らかに、この巨大なキャッシュサイズでは、キャッシュヒット率が高く、これにより、優れた、または超線形の高速化が観察されたことがわかります。n=231

2番目の質問については、現在のアーキテクチャでは、データの時間的および空間的な局所性を活用するために、必要に応じてキャッシュラインを削除して置き換えることにより、データを既にプリフェッチしています。しかし、2048メガバイトのデータを処理するシングルコアにはこれでは十分ではありません。を約170メガバイトに制限すると、同じ条件で(多かれ少なかれ正確ではない)実行されているため、単一のコアで多かれ少なかれ同じパフォーマンスが見られるはずです。n

最後に、QSM(Queueing Shared Memory)以外に、共有メモリへのアクセスの競合を同じレベルで考慮する他の理論的な並列モデルを知りません(OpenMPを使用する場合、メインメモリはコア間で共有されます) 、キャッシュはコア間でも常に共有されます)。とにかく、このモデルは興味深いものですが、大きな成功を収めることはできませんでした。


1
また、これは、各コアが多かれ少なかれ一定量のメモリレベルの並列処理、たとえば、所定の時間に処理中の10 x []ロードを提供するものと見なすと役立つ場合があります。共有L3でヒットする可能性が0.5%の場合、1つのスレッドで0.995 ** 10(95 +%)の確率ですべてのロードがメインメモリの応答を待機する必要があります。6つのコアが合計60 x []の保留中の読み取りを提供するため、L3で少なくとも1つの読み取りがヒットする可能性はほぼ26%です。さらに、MLPが多いほど、メモリコントローラーはアクセスをスケジュールして実際の帯域幅を増やすことができます。
ポールA.クレイトン

5

私は__builtin_prefetch()を自分で試してみることにしました。他の人が自分のマシンでテストしたい場合の答えとして、ここに投稿しています。結果は、Jukkaの説明に近いものです。20個の要素を先読みする場合と0個の要素を先読みする場合の実行時間は約20%短縮されます。

結果:

prefetch =   0, time = 1.58000
prefetch =   1, time = 1.47000
prefetch =   2, time = 1.39000
prefetch =   3, time = 1.34000
prefetch =   4, time = 1.31000
prefetch =   5, time = 1.30000
prefetch =   6, time = 1.27000
prefetch =   7, time = 1.28000
prefetch =   8, time = 1.26000
prefetch =   9, time = 1.27000
prefetch =  10, time = 1.27000
prefetch =  11, time = 1.27000
prefetch =  12, time = 1.30000
prefetch =  13, time = 1.29000
prefetch =  14, time = 1.30000
prefetch =  15, time = 1.28000
prefetch =  16, time = 1.24000
prefetch =  17, time = 1.28000
prefetch =  18, time = 1.29000
prefetch =  19, time = 1.25000
prefetch =  20, time = 1.24000
prefetch =  19, time = 1.26000
prefetch =  18, time = 1.27000
prefetch =  17, time = 1.26000
prefetch =  16, time = 1.27000
prefetch =  15, time = 1.28000
prefetch =  14, time = 1.29000
prefetch =  13, time = 1.26000
prefetch =  12, time = 1.28000
prefetch =  11, time = 1.30000
prefetch =  10, time = 1.31000
prefetch =   9, time = 1.27000
prefetch =   8, time = 1.32000
prefetch =   7, time = 1.31000
prefetch =   6, time = 1.30000
prefetch =   5, time = 1.27000
prefetch =   4, time = 1.33000
prefetch =   3, time = 1.38000
prefetch =   2, time = 1.41000
prefetch =   1, time = 1.41000
prefetch =   0, time = 1.59000

コード:

#include <stdlib.h>
#include <time.h>
#include <stdio.h>

void cracker(int *y, int *x, int *p, int n, int pf) {
    int i;
    int saved = pf;  /* let compiler optimize address computations */

    for (i = 0; i < n; i++) {
        __builtin_prefetch(&x[p[i+saved]]);
        y[i] += x[p[i]];
    }
}

int main(void) {
    int n = 50000000;
    int *x, *y, *p, i, pf, k;
    clock_t start, stop;
    double elapsed;

    /* set up arrays */
    x = malloc(sizeof(int)*n);
    y = malloc(sizeof(int)*n);
    p = malloc(sizeof(int)*n);
    for (i = 0; i < n; i++)
        p[i] = rand()%n;

    /* warm-up exercise */
    cracker(y, x, p, n, pf);

    k = 20;
    for (pf = 0; pf < k; pf++) {
        start = clock();
        cracker(y, x, p, n, pf);
        stop = clock();
        elapsed = ((double)(stop-start))/CLOCKS_PER_SEC;
        printf("prefetch = %3d, time = %.5lf\n", pf, elapsed);
    }
    for (pf = k; pf >= 0; pf--) {
        start = clock();
        cracker(y, x, p, n, pf);
        stop = clock();
        elapsed = ((double)(stop-start))/CLOCKS_PER_SEC;
        printf("prefetch = %3d, time = %.5lf\n", pf, elapsed);
    }

    return 0;
}

4
  1. DDR3アクセスは確かにパイプライン化されています。http://www.eng.utah.edu/~cs7810/pres/dram-cs7810-protocolx2.pdfスライド20および24は、パイプライン読み取り操作中にメモリバスで何が起こるかを示しています。

  2. (一部間違っています。以下を参照)CPUアーキテクチャがキャッシュプリフェッチをサポートしている場合、複数のスレッドは必要ありません。最新のx86とARM、および他の多くのアーキテクチャには、明示的なプリフェッチ命令があります。さらに多くは、メモリアクセスのパターンを検出し、プリフェッチを自動的に実行しようとします。ソフトウェアサポートはコンパイラ固有です。たとえば、GCCおよびClangには、明示的なプリフェッチ用の__builtin_prefech()組み込み関数があります。

Intelスタイルのハイパースレッディングは、キャッシュミスを待つことにほとんどの時間を費やしているプログラムでは非常にうまく機能するようです。私の経験では、計算集中型のワークロードでは、物理コアの数を超えるスピードアップはほとんどありません。

編集:私はポイント2で間違っていました。プリフェッチはシングルコアのメモリアクセスを最適化できますが、複数のコアの合計メモリ帯域幅はシングルコアの帯域幅よりも大きいようです。どれだけ大きいかは、CPUに依存します。

ハードウェアプリフェッチャーとその他の最適化を組み合わせることで、ベンチマークが非常に難しくなります。明示的なプリフェッチがパフォーマンスに非常に目に見えるまたは存在しない影響を与えるケースを構築することが可能です。このベンチマークは後者の1つです。


__builtin_prefechは非常に有望です。残念なことに、私の簡単な実験では、シングルスレッドのパフォーマンスを大幅に改善することはできなかったようです(<10%)。この種のアプリケーションでは、どの程度の速度改善が期待できますか?
ユッカスオメラ

もっと期待した。プリフェッチがDSPとゲームに大きな影響を与えることを知っているので、自分で実験しなければなりませんでした。判明ウサギの穴が...深い行く
Juhani Simola

私の最初の試みは、配列に格納された固定のランダムな順序を作成し、プリフェッチ(gist.github.com/osimola/7917602)の有無にかかわらず、その順序で反復することでした。これにより、Core i5で約2%の差が生じました。プリフェッチがまったく機能しないか、ハードウェアプレディクタがインダイレクションを理解しているように聞こえます。
ジュハニシモラ

1
したがって、そのためのテストとして、2回目の試行(gist.github.com/osimola/7917568)は、固定ランダムシードによって生成された順にメモリにアクセスします。今回は、プリフェッチバージョンは非プリフェッチの約2倍、1ステップ先のプリフェッチよりも3倍高速でした。プリフェッチバージョンは、メモリアクセスごとに、非プリフェッチバージョンよりも多くの計算を行うことに注意してください。
ジュハニシモラ

これはマシンに依存しているようです。以下のPat Morinのコードを試してみました(評判がないのでその投稿にコメントできません)。結果は、プリフェッチ値が異なる場合でも1.3%以内です。
Juhani Simola
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.