37

プログラムのパフォーマンスに対するベクトル化の影響を調査しています。これに関して、私は次のコードを書きました:

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

#define LEN 10000000

int main(){

    struct timeval stTime, endTime;

    double* a = (double*)malloc(LEN*sizeof(*a));
    double* b = (double*)malloc(LEN*sizeof(*b));
    double* c = (double*)malloc(LEN*sizeof(*c));

    int k;
    for(k = 0; k < LEN; k++){
        a[k] = rand();
        b[k] = rand();
    }

    gettimeofday(&stTime, NULL);

    for(k = 0; k < LEN; k++)
        c[k] = a[k] * b[k];

    gettimeofday(&endTime, NULL);

    FILE* fh = fopen("dump", "w");
    for(k = 0; k < LEN; k++)
        fprintf(fh, "c[%d] = %f\t", k, c[k]);
    fclose(fh);

    double timeE = (double)(endTime.tv_usec + endTime.tv_sec*1000000 - stTime.tv_usec - stTime.tv_sec*1000000);

    printf("Time elapsed: %f\n", timeE);

    return 0;
}

このコードでは、単純に 2 つのベクトルを初期化して乗算しています。結果は vector に保存されcます。私が主に興味を持っているのは、次のループをベクトル化する効果です。

for(k = 0; k < LEN; k++)
    c[k] = a[k] * b[k];

次の 2 つのコマンドを使用してコードをコンパイルします。

1) icc -O2 TestSMID.c -o TestSMID -no-vec -no-simd
2) icc -O2 TestSMID.c -o TestSMID -vec-report2

2 番目のコマンドがループを正常にベクトル化するため、パフォーマンスが向上することを期待しています。しかし、私の調査によると、ループをベクトル化してもパフォーマンスは向上しません。

私はトピックにあまり詳しくないので、ここで何かを見落としているかもしれません。私のコードに何か問題がある場合はお知らせください。

よろしくお願いします。

PS: 私は Mac OSX を使用しているため、割り当てられたメモリはすべて 16 バイト アラインされているため、データをアラインする必要はありません。

編集: まず、コメントと回答をくださった皆様に感謝いたします。@Mysticial によって提案された答えについて考えましたが、ここで言及すべき点がいくつかあります。まず、@Vinska が述べたように、c[k]=a[k]*b[k]1 サイクルしかかかりません。ループ インデックスのインクリメントとkが よりも小さいことを確認するための比較に加えてLEN、操作を実行するために行うべきことが他にもあります。コンパイラによって生成されたアセンブリ コードを見ると、単純な乗算には 1 サイクルよりもはるかに多くのサイクルが必要であることがわかります。ベクトル化されたバージョンは次のようになります。

L_B1.9:                         # Preds L_B1.8
        movq      %r13, %rax                                    #25.5
        andq      $15, %rax                                     #25.5
        testl     %eax, %eax                                    #25.5
        je        L_B1.12       # Prob 50%                      #25.5
                                # LOE rbx r12 r13 r14 r15 eax
L_B1.10:                        # Preds L_B1.9
        testb     $7, %al                                       #25.5
        jne       L_B1.32       # Prob 10%                      #25.5
                                # LOE rbx r12 r13 r14 r15
L_B1.11:                        # Preds L_B1.10
        movsd     (%r14), %xmm0                                 #26.16
        movl      $1, %eax                                      #25.5
        mulsd     (%r15), %xmm0                                 #26.23
        movsd     %xmm0, (%r13)                                 #26.9
                                # LOE rbx r12 r13 r14 r15 eax
L_B1.12:                        # Preds L_B1.11 L_B1.9
        movl      %eax, %edx                                    #25.5
        movl      %eax, %eax                                    #26.23
        negl      %edx                                          #25.5
        andl      $1, %edx                                      #25.5
        negl      %edx                                          #25.5
        addl      $10000000, %edx                               #25.5
        lea       (%r15,%rax,8), %rcx                           #26.23
        testq     $15, %rcx                                     #25.5
        je        L_B1.16       # Prob 60%                      #25.5
                                # LOE rdx rbx r12 r13 r14 r15 eax
L_B1.13:                        # Preds L_B1.12
        movl      %eax, %eax                                    #25.5
                                # LOE rax rdx rbx r12 r13 r14 r15
L_B1.14:                        # Preds L_B1.14 L_B1.13
        movups    (%r15,%rax,8), %xmm0                          #26.23
        movsd     (%r14,%rax,8), %xmm1                          #26.16
        movhpd    8(%r14,%rax,8), %xmm1                         #26.16
        mulpd     %xmm0, %xmm1                                  #26.23
        movntpd   %xmm1, (%r13,%rax,8)                          #26.9
        addq      $2, %rax                                      #25.5
        cmpq      %rdx, %rax                                    #25.5
        jb        L_B1.14       # Prob 99%                      #25.5
        jmp       L_B1.20       # Prob 100%                     #25.5
                                # LOE rax rdx rbx r12 r13 r14 r15
L_B1.16:                        # Preds L_B1.12
        movl      %eax, %eax                                    #25.5
                                # LOE rax rdx rbx r12 r13 r14 r15
L_B1.17:                        # Preds L_B1.17 L_B1.16
        movsd     (%r14,%rax,8), %xmm0                          #26.16
        movhpd    8(%r14,%rax,8), %xmm0                         #26.16
        mulpd     (%r15,%rax,8), %xmm0                          #26.23
        movntpd   %xmm0, (%r13,%rax,8)                          #26.9
        addq      $2, %rax                                      #25.5
        cmpq      %rdx, %rax                                    #25.5
        jb        L_B1.17       # Prob 99%                      #25.5
                                # LOE rax rdx rbx r12 r13 r14 r15
L_B1.18:                        # Preds L_B1.17
        mfence                                                  #25.5
                                # LOE rdx rbx r12 r13 r14 r15
L_B1.19:                        # Preds L_B1.18
        mfence                                                  #25.5
                                # LOE rdx rbx r12 r13 r14 r15
L_B1.20:                        # Preds L_B1.14 L_B1.19 L_B1.32
        cmpq      $10000000, %rdx                               #25.5
        jae       L_B1.24       # Prob 0%                       #25.5
                                # LOE rdx rbx r12 r13 r14 r15
L_B1.22:                        # Preds L_B1.20 L_B1.22
        movsd     (%r14,%rdx,8), %xmm0                          #26.16
        mulsd     (%r15,%rdx,8), %xmm0                          #26.23
        movsd     %xmm0, (%r13,%rdx,8)                          #26.9
        incq      %rdx                                          #25.5
        cmpq      $10000000, %rdx                               #25.5
        jb        L_B1.22       # Prob 99%                      #25.5
                                # LOE rdx rbx r12 r13 r14 r15
L_B1.24:                        # Preds L_B1.22 L_B1.20

そして、ベクトル化されていないバージョンは次のとおりです。

L_B1.9:                         # Preds L_B1.8
        xorl      %eax, %eax                                    #25.5
                                # LOE rbx r12 r13 r14 r15 eax
L_B1.10:                        # Preds L_B1.10 L_B1.9
        lea       (%rax,%rax), %edx                             #26.9
        incl      %eax                                          #25.5
        cmpl      $5000000, %eax                                #25.5
        movsd     (%r15,%rdx,8), %xmm0                          #26.16
        movsd     8(%r15,%rdx,8), %xmm1                         #26.16
        mulsd     (%r13,%rdx,8), %xmm0                          #26.23
        mulsd     8(%r13,%rdx,8), %xmm1                         #26.23
        movsd     %xmm0, (%rbx,%rdx,8)                          #26.9
        movsd     %xmm1, 8(%rbx,%rdx,8)                         #26.9
        jb        L_B1.10       # Prob 99%                      #25.5
                                # LOE rbx r12 r13 r14 r15 eax

これに加えて、プロセッサは 24 バイトしかロードしません。メモリへのアクセスごとに、1 行 (64 バイト) がロードされます。さらに重要なことにa、 、b、および に必要なメモリcは連続しているため、プリフェッチャーは間違いなく大いに役立ち、次のブロックを事前にロードします。そうは言っても、@Mystial で計算されたメモリ帯域幅は悲観的すぎると思います。

さらに、SIMD を使用して非常に単純な追加のプログラムのパフォーマンスを向上させる方法については、Intel Vectorization Guideに記載されています。したがって、この非常に単純なループのパフォーマンスをいくらか改善できるはずです。

Edit2: コメントありがとうございます。また、@Mysticial のサンプル コードのおかげで、SIMD によるパフォーマンス向上の効果がようやくわかりました。Mystial が述べたように、問題はメモリ帯域幅でした。ab、およびcL1 キャッシュに収まる小さいサイズを選択すると、SIMD がパフォーマンスを大幅に向上させるのに役立つことがわかります。私が得た結果は次のとおりです。

icc -O2 -o TestSMIDNoVec -no-vec TestSMID2.c: 17.34 sec

icc -O2 -o TestSMIDVecNoUnroll -vec-report2 TestSMID2.c: 9.33 sec

ループを展開すると、パフォーマンスがさらに向上します。

icc -O2 -o TestSMIDVecUnroll -vec-report2 TestSMID2.c -unroll=8: 8.6sec

また、 でコンパイルした場合、プロセッサが反復を完了するのに 1 サイクルしかかからないことにも言及する必要があります-O2

PS: 私のコンピューターは Macbook Pro Core i5 @2.5GHz (デュアルコア) です。

4

4 に答える 4

78

この元の回答は2013年に有効でした。2017年のハードウェアの時点で、質問と回答の両方が古くなっているほど状況が変化しています。

2017 年の更新については、この回答の最後を参照してください。


元の回答 (2013):

メモリ帯域幅がボトルネックになっているためです。

ベクトル化やその他のマイクロ最適化によって計算速度は向上しますが、メモリの速度は向上しません。

あなたの例では:

for(k = 0; k < LEN; k++)
    c[k] = a[k] * b[k];

ほとんど作業を行わずに、すべてのメモリに対して単一のパスを作成しています。これはメモリ帯域幅を使い果たしています。

したがって、最適化の方法 (ベクトル化、アンロールなど) に関係なく、それほど高速になることはありません。


2013 年の典型的なデスクトップ マシンのメモリ帯域幅は10 GB/秒程度です*。
ループは24 バイト/反復に触れます。

ベクトル化がなければ、最新の x64 プロセッサはおそらく 1 サイクルあたり約 1 回の反復を実行できます*。

4 GHz で実行しているとします。

  • (4 * 10^9) * 24 bytes/iteration = 96 GB/s

これは、ベクトル化を行わない場合のメモリ帯域幅のほぼ 10 倍です。


*驚くべきことではありませんが、引用をしなかったため、上記の数字を疑う人が何人かいました. まあ、それらは経験から私の頭のてっぺんから外れていました。そこで、それを証明するベンチマークをいくつか示します。

ループの反復は、1 サイクル/反復の速さで実行できます。

LENキャッシュに収まるように削減すれば、メモリのボトルネックを取り除くことができます。
(C++ の方が簡単だったので、これをテストしましたが、違いはありません。)

#include <iostream>
#include <time.h>
using std::cout;
using std::endl;

int main(){
    const int LEN = 256;

    double *a = (double*)malloc(LEN*sizeof(*a));
    double *b = (double*)malloc(LEN*sizeof(*a));
    double *c = (double*)malloc(LEN*sizeof(*a));

    int k;
    for(k = 0; k < LEN; k++){
        a[k] = rand();
        b[k] = rand();
    }

    clock_t time0 = clock();

    for (int i = 0; i < 100000000; i++){
        for(k = 0; k < LEN; k++)
            c[k] = a[k] * b[k];
    }

    clock_t time1 = clock();
    cout << (double)(time1 - time0) / CLOCKS_PER_SEC << endl;
}
  • プロセッサ: Intel Core i7 2600K @ 4.2 GHz
  • コンパイラ: Visual Studio 2012
  • 時間: 6.55 秒

このテストでは、わずか6.55秒で 25,600,000,000 回の反復を実行しました。

  • 6.55 * 4.2 GHz= 27,510,000,000 サイクル
  • 27,510,000,000 / 25,600,000,000= 1.074 サイクル/反復

どうすればできるのか疑問に思っている場合は、次のようにします。

  • 2回のロード
  • 1店舗
  • 1乗算
  • インクリメントカウンター
  • 比較 + 分岐

すべて1サイクルで...

それは、最新のプロセッサーとコンパイラーが素晴らしいからです。

これらの各演算にはレイテンシがありますが (特に乗算)、プロセッサは同時に複数の反復を実行できます。私のテスト マシンは Sandy Bridge プロセッサで、1 サイクルごとに 2x128b ロード、1x128b ストア、および 1x256b ベクトル FP 乗算を維持できます。また、ロードがマイクロ融合 uops のメモリ ソース オペランドである場合は、別の 1 つまたは 2 つのベクトルまたは整数演算が必要になる可能性があります。(256b AVX ロード/ストアを使用する場合のみ、2 ロード + 1 ストア スループット。それ以外の場合は、サイクルごとに合計 2 つのメモリ操作のみ (最大 1 ストア))。

アセンブリー (簡潔にするために省略します) を見ると、コンパイラーがループをアンロールし、それによってループのオーバーヘッドが削減されたようです。しかし、それをベクトル化することはできませんでした。


メモリ帯域幅は 10 GB/秒程度です。

これをテストする最も簡単な方法は、次の方法memset()です。

#include <iostream>
#include <time.h>
using std::cout;
using std::endl;

int main(){
    const int LEN = 1 << 30;    //  1GB

    char *a = (char*)calloc(LEN,1);

    clock_t time0 = clock();

    for (int i = 0; i < 100; i++){
        memset(a,0xff,LEN);
    }

    clock_t time1 = clock();
    cout << (double)(time1 - time0) / CLOCKS_PER_SEC << endl;
}
  • プロセッサ: Intel Core i7 2600K @ 4.2 GHz
  • コンパイラ: Visual Studio 2012
  • 時間: 5.811 秒

したがって、私のマシンが 100 GB のメモリに書き込むのに 5.811秒かかります。それは約17.2 GB/sです。

そして、私のプロセッサーはハイエンドです。Nehalem および Core 2 世代のプロセッサは、メモリ帯域幅が少なくなっています。


2017 年 3 月の更新:

2017 年現在、状況はさらに複雑になっています。

DDR4 とクアッドチャネル メモリのおかげで、シングル スレッドがメモリ帯域幅を飽和させることはなくなりました。しかし、帯域幅の問題がなくなるわけではありません。帯域幅は向上しましたが、プロセッサ コアも改善されており、その数も増えています。

数学的に言えば:

  • 各コアには帯域幅制限がありますX
  • メイン メモリには の帯域幅制限がありYます。
  • 古いシステムでは、X > Y.
  • 現在のハイエンド システムでは、X < Y. しかしX * (# of cores) > Y

2013 年: Sandy Bridge @ 4 GHz + デュアルチャネル DDR3 @ 1333 MHz

  • ベクトル化なし (8 バイトのロード/ストア):X = 32 GB/sおよびY = ~17 GB/s
  • ベクトル化された SSE* (16 バイトのロード/ストア):X = 64 GB/sおよびY = ~17 GB/s

2017 年現在: Haswell-E @ 4 GHz + クアッドチャネル DDR4 @ 2400 MHz

  • ベクトル化なし (8 バイトのロード/ストア):X = 32 GB/sおよびY = ~70 GB/s
  • ベクトル化された AVX* (32 バイトのロード/ストア):X = 64 GB/sおよびY = ~70 GB/s

(Sandy Bridge と Haswell の両方で、キャッシュのアーキテクチャ上の制限により、帯域幅は SIMD 幅に関係なく約 16 バイト/サイクルに制限されます。)

そのため、今日では、単一のスレッドが常にメモリ帯域幅を飽和させることができるとは限りません。そして、その制限を達成するには、ベクトル化する必要がありますXYただし、2 つ以上のスレッドを使用すると、依然としてメイン メモリの帯域幅の制限に達します。

しかし、変更されていないことが 1 つあります。おそらく今後も変更されることはありません。すべてのコアで帯域幅を大量に消費するループを実行しても、メモリ帯域幅全体が飽和状態になることはありません。

于 2013-08-10T07:02:59.160 に答える
2

編集:答えをたくさん修正しました。また、Mystical の回答が完全に正しくないことについて以前に書いたことのほとんどを無視してください。ただし、非常にさまざまなテストを行ったにもかかわらず、元のコードがメモリ速度に拘束されている兆候は見られなかったため、メモリがボトルネックになっていることにまだ同意しません。その間、CPU バウンドの明確な兆候を示し続けました。


多くの理由が考えられます。その理由はハードウェアに大きく依存する可能性があるため、推測に基づいて推測するべきではないと判断しました。後でテスト中に遭遇したこれらのことの概要を説明します。そこでは、はるかに正確で信頼性の高い CPU 時間測定方法を使用し、ループを 1000 回繰り返しました。この情報が役立つと思います。ただし、ハードウェアに依存するため、一粒の塩で考えてください。

  • SSE ファミリーの命令を使用した場合、得られたベクトル化されたコードは、ベクトル化されていないコードよりも 10% 以上高速でした。
  • SSE ファミリーを使用してベクトル化されたコードと、AVX を使用してベクトル化されたコードは、ほぼ同じパフォーマンスで実行されました。
  • AVX 命令を使用した場合、ベクトル化されていないコードが最も速く実行され、私が試した他のどのコードよりも 25% 以上高速でした。
  • すべてのケースで、結果は CPU クロックに比例してスケーリングされます。
  • 結果はメモリ クロックの影響をほとんど受けませんでした。
  • 結果はメモリ レイテンシの影響をかなり受けました。メモリ クロックよりもはるかに大きく、CPU クロックほどではありませんでした。

クロックごとにほぼ 1 回の反復を実行する WRT Mystical の例 - CPU スケジューラがそれほど効率的であるとは予想していませんでした。しかし、驚いたことに、そうではありません。確かに間違ってましたね、すみません。私自身の CPU はさらに効率的に実行しました - 1.048 サイクル/反復。したがって、Mystical の回答のこの部分が間違いなく正しいことを証明できます。

于 2013-08-10T12:32:58.483 に答える
0

a[] b[] と c[] が L2 キャッシュをめぐって争っている場合に備えて ::

#include <string.h> /* for memcpy */

 ...

 gettimeofday(&stTime, NULL);

    for(k = 0; k < LEN; k += 4) {
        double a4[4], b4[4], c4[4];
        memcpy(a4,a+k, sizeof a4);
        memcpy(b4,b+k, sizeof b4);
        c4[0] = a4[0] * b4[0];
        c4[1] = a4[1] * b4[1];
        c4[2] = a4[2] * b4[2];
        c4[3] = a4[3] * b4[3];
        memcpy(c+k,c4, sizeof c4);
        }

    gettimeofday(&endTime, NULL);

実行時間を 98429.000000 から 67213.000000 に短縮します。ループを 8 倍に展開すると、ここでは 57157.000000 に減少します。

于 2013-08-10T13:56:25.743 に答える