32ビットのループカウンターを64ビットで置き換えると、Intel CPUで_mm_popcnt_u64を使用すると、パフォーマンスが大幅にずれる


1424

popcount大規模なデータ配列への最速の方法を探していました。私が遭遇した非常に奇妙な効果を:からループ変数を変更するunsigneduint64_t私のPC上で50%で作られたパフォーマンスの低下を。

ベンチマーク

#include <iostream>
#include <chrono>
#include <x86intrin.h>

int main(int argc, char* argv[]) {

    using namespace std;
    if (argc != 2) {
       cerr << "usage: array_size in MB" << endl;
       return -1;
    }

    uint64_t size = atol(argv[1])<<20;
    uint64_t* buffer = new uint64_t[size/8];
    char* charbuffer = reinterpret_cast<char*>(buffer);
    for (unsigned i=0; i<size; ++i)
        charbuffer[i] = rand()%256;

    uint64_t count,duration;
    chrono::time_point<chrono::system_clock> startP,endP;
    {
        startP = chrono::system_clock::now();
        count = 0;
        for( unsigned k = 0; k < 10000; k++){
            // Tight unrolled loop with unsigned
            for (unsigned i=0; i<size/8; i+=4) {
                count += _mm_popcnt_u64(buffer[i]);
                count += _mm_popcnt_u64(buffer[i+1]);
                count += _mm_popcnt_u64(buffer[i+2]);
                count += _mm_popcnt_u64(buffer[i+3]);
            }
        }
        endP = chrono::system_clock::now();
        duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
        cout << "unsigned\t" << count << '\t' << (duration/1.0E9) << " sec \t"
             << (10000.0*size)/(duration) << " GB/s" << endl;
    }
    {
        startP = chrono::system_clock::now();
        count=0;
        for( unsigned k = 0; k < 10000; k++){
            // Tight unrolled loop with uint64_t
            for (uint64_t i=0;i<size/8;i+=4) {
                count += _mm_popcnt_u64(buffer[i]);
                count += _mm_popcnt_u64(buffer[i+1]);
                count += _mm_popcnt_u64(buffer[i+2]);
                count += _mm_popcnt_u64(buffer[i+3]);
            }
        }
        endP = chrono::system_clock::now();
        duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
        cout << "uint64_t\t"  << count << '\t' << (duration/1.0E9) << " sec \t"
             << (10000.0*size)/(duration) << " GB/s" << endl;
    }

    free(charbuffer);
}

ご覧のとおり、ランダムなデータのバッファーを作成します。サイズはxメガバイトでx、コマンドラインから読み取られます。その後、バッファを反復処理し、展開されたバージョンのx86 popcount組み込み関数を使用してポップカウントを実行します。より正確な結果を得るために、popcountを10,000回実行しています。ポップカウントの時間を測定します。大文字の場合、内部ループ変数はunsignedであり、小文字の場合、内部ループ変数はuint64_tです。これで違いはないと思いましたが、その逆です。

(絶対にクレイジーな)結果

私はそれを次のようにコンパイルします(g ++バージョン:Ubuntu 4.8.2-19ubuntu1):

g++ -O3 -march=native -std=c++11 test.cpp -o test

これは、実行中のHaswell Core i7-4770K CPU @ 3.50 GHzでの結果ですtest 1(1 MBのランダムデータ)。

  • 署名なし41959360000 0.401554秒 26.113 GB / s
  • uint64_t 41959360000 0.759822秒 13.8003 GB /秒

ご覧のとおり、uint64_tバージョンのスループットはバージョンのスループットの半分すぎませunsignedん。問題は異なるアセンブリが生成されることのようですが、なぜですか?まず、コンパイラのバグを考えたので、試しましたclang++(Ubuntu Clangバージョン3.4-1ubuntu3):

clang++ -O3 -march=native -std=c++11 teest.cpp -o test

結果: test 1

  • 署名なし41959360000 0.398293秒 26.3267 GB / s
  • uint64_t 41959360000 0.680954秒 15.3986 GB /秒

したがって、それはほとんど同じ結果であり、まだ奇妙です。しかし、今では超奇妙になっています。入力から読み取られたバッファサイズを定数に置き換える1ため、次のように変更します。

uint64_t size = atol(argv[1]) << 20;

uint64_t size = 1 << 20;

したがって、コンパイラーはコンパイル時にバッファー・サイズを認識します。多分それはいくつかの最適化を追加することができます!の番号はg++次のとおりです。

  • 署名なし41959360000 0.509156秒 20.5944 GB / s
  • uint64_t 41959360000 0.508673秒 20.6139 GB / s

現在、両方のバージョンが同等に高速です。しかし、unsigned さらに遅くなりました!それはからに低下した26ため20 GB/s、非定数を定数値に置き換えると、最適化が失われます。真剣に、私はここで何が起こっているのか手がかりがありません!しかし、今clang++は新しいバージョンです:

  • 署名なし41959360000 0.677009秒 15.4884 GB / s
  • uint64_t 41959360000 0.676909秒 15.4906 GB / s

待って、何?現在、どちらのバージョンも15 GB /秒という遅い数値に落ちています。したがって、非定数を定数値で置き換えると、Clangのどちらの場合でもコードが遅くなります!

Ivy Bridge CPUを使用している同僚にベンチマークをコンパイルするよう依頼しました。彼は同様の結果を得たので、Haswellではないようです。ここでは2つのコンパイラが奇妙な結果を生成するため、コンパイラのバグでもないようです。ここにはAMD CPUがないため、Intelでのみテストできます。

もっと狂ってください!

最初の例(を含む例)を取り、変数の前atol(argv[1])にaを配置staticします。

static uint64_t size=atol(argv[1])<<20;

これがg ++での私の結果です:

  • 署名なし41959360000 0.396728秒 26.4306 GB / s
  • uint64_t 41959360000 0.509484秒 20.5811 GB /秒

いや、さらに別の選択肢。まだ26 GB /秒の高速ですu32u64、少なくとも13 GB /秒から20 GB /秒のバージョンに到達できました。同僚のPCでは、u64バージョンがバージョンよりもさらに高速になりu32、すべての中で最も速い結果が得られました。悲しいことに、これはに対してのみ機能しg++clang++気にしないようですstatic

私の質問

これらの結果を説明できますか?特に:

  • どのようにとの間のこのような違いがあることができu32、およびu64
  • 非定数を一定のバッファサイズで置き換えると、最適でないコードがトリガーされます
  • staticキーワードを挿入すると、u64ループが速くなりますか?同僚のコンピューターの元のコードよりもさらに高速です!

私は最適化が難しい領域であることを知っていますが、そのような小さな変更が実行時間の100%の違いにつながることや、一定のバッファーサイズなどの小さな要素が結果を完全に混ぜることができるとは思いもしませんでした。もちろん、私は常に26 GB /秒をポップカウントできるバージョンが欲しいです。私が考えることができる唯一の信頼できる方法は、このケースのアセンブリをコピーして貼り付け、インラインアセンブリを使用することです。これは、小さな変更で狂ったように見えるコンパイラを取り除くことができる唯一の方法です。どう思いますか?ほとんどのパフォーマンスでコードを確実に取得する別の方法はありますか?

分解

以下は、さまざまな結果の分解です。

g ++ / u32 / non-const bufsizeからの26 GB / sバージョン:

0x400af8:
lea 0x1(%rdx),%eax
popcnt (%rbx,%rax,8),%r9
lea 0x2(%rdx),%edi
popcnt (%rbx,%rcx,8),%rax
lea 0x3(%rdx),%esi
add %r9,%rax
popcnt (%rbx,%rdi,8),%rcx
add $0x4,%edx
add %rcx,%rax
popcnt (%rbx,%rsi,8),%rcx
add %rcx,%rax
mov %edx,%ecx
add %rax,%r14
cmp %rbp,%rcx
jb 0x400af8

g ++ / u64 / non-const bufsizeからの13 GB / sバージョン:

0x400c00:
popcnt 0x8(%rbx,%rdx,8),%rcx
popcnt (%rbx,%rdx,8),%rax
add %rcx,%rax
popcnt 0x10(%rbx,%rdx,8),%rcx
add %rcx,%rax
popcnt 0x18(%rbx,%rdx,8),%rcx
add $0x4,%rdx
add %rcx,%rax
add %rax,%r12
cmp %rbp,%rdx
jb 0x400c00

15 GB / sバージョンのclang ++ / u64 / non-const bufsize

0x400e50:
popcnt (%r15,%rcx,8),%rdx
add %rbx,%rdx
popcnt 0x8(%r15,%rcx,8),%rsi
add %rdx,%rsi
popcnt 0x10(%r15,%rcx,8),%rdx
add %rsi,%rdx
popcnt 0x18(%r15,%rcx,8),%rbx
add %rdx,%rbx
add $0x4,%rcx
cmp %rbp,%rcx
jb 0x400e50

20 GB /秒バージョンのg ++ / u32&u64 / const bufsize

0x400a68:
popcnt (%rbx,%rdx,1),%rax
popcnt 0x8(%rbx,%rdx,1),%rcx
add %rax,%rcx
popcnt 0x10(%rbx,%rdx,1),%rax
add %rax,%rcx
popcnt 0x18(%rbx,%rdx,1),%rsi
add $0x20,%rdx
add %rsi,%rcx
add %rcx,%rbp
cmp $0x100000,%rdx
jne 0x400a68

15 GB /秒バージョンのclang ++ / u32&u64 / const bufsize

0x400dd0:
popcnt (%r14,%rcx,8),%rdx
add %rbx,%rdx
popcnt 0x8(%r14,%rcx,8),%rsi
add %rdx,%rsi
popcnt 0x10(%r14,%rcx,8),%rdx
add %rsi,%rdx
popcnt 0x18(%r14,%rcx,8),%rbx
add %rdx,%rbx
add $0x4,%rcx
cmp $0x20000,%rcx
jb 0x400dd0

興味深いことに、最速(26 GB /秒)バージョンも最長です。を使用する唯一のソリューションのようleaです。jbジャンプに使用するバージョンもあれば、を使用するバージョンもありますjne。しかし、それを除いて、すべてのバージョンは同等であるようです。100%のパフォーマンスギャップがどこから発生するかはわかりませんが、アセンブリを解読することに長けていません。最も遅いバージョン(13 GB /秒)でも、非常に短くて見栄えがします。誰かがこれを説明できますか?

学んだ教訓

この質問の答えがどうなるかは関係ありません。本当にホットなループでは、すべての詳細が重要になる可能性があることを知りました。ホットコードに関連付けられていないように見える詳細もです。ループ変数にどのタイプを使用するか考えたことはありませんが、このような小さな変更で100%の違いが生じる可能性があります。staticサイズ変数の前にキーワードを挿入することで見たように、バッファのストレージタイプでも大きな違いを生む可能性があります。将来的には、システムパフォーマンスに不可欠な非常にタイトでホットなループを作成するときに、さまざまなコンパイラーでさまざまな代替手段を常にテストします。

興味深いのは、ループを4回展開したにもかかわらず、パフォーマンスの差が依然として非常に大きいことです。したがって、展開しても、パフォーマンスの大幅な逸脱に見舞われる可能性があります。かなり興味深い。


8
とてもたくさんのコメント!あなたはできるチャットでそれらを表示しても、あなたがしたい場合はそこに自分自身を残して、より多くのここにを追加しないでください!
Shog9 16

3
GCC Issue 62011、popcnt命令のFalse Data Dependencyも参照してください。他の誰かがそれを提供しましたが、クリーンアップ中に失われたようです。
jww

わかりませんが、静的バージョンの逆アセンブリの1つですか?そうでない場合は、投稿を編集して追加できますか?
ケリーS.フランス語

回答:


1552

犯人:偽のデータ依存関係(そしてコンパイラーはそれを認識していません)

Sandy / Ivy BridgeおよびHaswellプロセッサでは、命令は次のとおりです。

popcnt  src, dest

デスティネーションレジスタに誤った依存関係があるようですdest。命令はそれにのみ書き込みを行いますが、dest実行の前に準備が整うまで待機します。この誤った依存関係は、Intelによってerratum HSD146(Haswell)およびSKL029(Skylake)として(現在)文書化されています。

Skylakeマイクロアーキテクチャは、これを固定lzcntしてtzcnt
キャノンレイク(およびアイスレイク)は、これを修正しましたpopcnt
bsf/ bsr真の出力依存関係があります:input = 0の出力は変更されません。(しかし、組み込み関数でそれを利用する方法はありません-AMDのみがそれを文書化し、コンパイラーはそれを公開しません。)

(はい、これらの命令はすべて同じ実行ユニットで実行さます)。


この依存関係はpopcnt、単一のループ反復からの4 秒を保持するだけではありません。ループの繰り返しを超えて実行できるため、プロセッサが異なるループの繰り返しを並列化することは不可能です。

unsigneduint64_tおよびその他の微調整は、直接問題に影響を与えません。ただし、レジスタを変数に割り当てるレジスタアロケータに影響します。

あなたの場合、速度は、レジスタアロケータが何をすることに決めたかに応じて、(偽の)依存関係チェーンにスタックしているものの直接的な結果です。

  • 13ギガバイト/秒鎖を有する:popcnt- add- popcnt- popcnt→次の反復
  • 15ギガバイト/秒のチェーンを持っていますpopcnt- add- popcnt- add→次の反復
  • 20 GB /秒にはチェーンがあります:popcnt- popcnt→次の反復
  • 26 GB / sにはチェーンがあります:popcnt- popcnt→次の反復

20 GB / sと26 GB / sの違いは、間接アドレッシングの小さなアーティファクトのようです。どちらにしても、この速度に達すると、プロセッサは他のボトルネックにぶつかり始めます。


これをテストするために、インラインアセンブリを使用してコンパイラをバイパスし、必要なアセンブリを正確に取得しました。また、count変数を分割して、ベンチマークを混乱させる可能性のある他のすべての依存関係を壊します。

結果は次のとおりです。

Sandy Bridge Xeon @ 3.5 GHz:(完全なテストコードは下部にあります)

  • GCC 4.6.3: g++ popcnt.cpp -std=c++0x -O3 -save-temps -march=native
  • Ubuntu 12

異なるレジスタ:18.6195 GB / s

.L4:
    movq    (%rbx,%rax,8), %r8
    movq    8(%rbx,%rax,8), %r9
    movq    16(%rbx,%rax,8), %r10
    movq    24(%rbx,%rax,8), %r11
    addq    $4, %rax

    popcnt %r8, %r8
    add    %r8, %rdx
    popcnt %r9, %r9
    add    %r9, %rcx
    popcnt %r10, %r10
    add    %r10, %rdi
    popcnt %r11, %r11
    add    %r11, %rsi

    cmpq    $131072, %rax
    jne .L4

同じレジスタ:8.49272 GB / s

.L9:
    movq    (%rbx,%rdx,8), %r9
    movq    8(%rbx,%rdx,8), %r10
    movq    16(%rbx,%rdx,8), %r11
    movq    24(%rbx,%rdx,8), %rbp
    addq    $4, %rdx

    # This time reuse "rax" for all the popcnts.
    popcnt %r9, %rax
    add    %rax, %rcx
    popcnt %r10, %rax
    add    %rax, %rsi
    popcnt %r11, %rax
    add    %rax, %r8
    popcnt %rbp, %rax
    add    %rax, %rdi

    cmpq    $131072, %rdx
    jne .L9

チェーンが壊れている同じレジスタ:17.8869 GB / s

.L14:
    movq    (%rbx,%rdx,8), %r9
    movq    8(%rbx,%rdx,8), %r10
    movq    16(%rbx,%rdx,8), %r11
    movq    24(%rbx,%rdx,8), %rbp
    addq    $4, %rdx

    # Reuse "rax" for all the popcnts.
    xor    %rax, %rax    # Break the cross-iteration dependency by zeroing "rax".
    popcnt %r9, %rax
    add    %rax, %rcx
    popcnt %r10, %rax
    add    %rax, %rsi
    popcnt %r11, %rax
    add    %rax, %r8
    popcnt %rbp, %rax
    add    %rax, %rdi

    cmpq    $131072, %rdx
    jne .L14

では、コンパイラの何が問題になっていますか?

GCCもVisual Studioも、popcntこのような誤った依存関係があることを認識していないようです。それにもかかわらず、これらの誤った依存関係は珍しいことではありません。コンパイラがそれを認識しているかどうかの問題です。

popcntは、最もよく使用される命令ではありません。したがって、主要なコンパイラがこのようなものを見落とす可能性があることは、それほど驚くことではありません。また、この問題を説明するドキュメントはどこにもないようです。インテルがそれを開示しない場合、誰かが偶然それにそれに遭遇するまで、外部の誰も知りません。

更新: バージョン4.9.2の時点で、GCCはこの誤った依存関係を認識し、最適化が有効な場合にそれを補正するコードを生成します。Clang、MSVC、さらにはIntel独自のICCを含む他のベンダーの主要なコンパイラーはまだ認識していませんこのマイクロアーキテクチャのエラータであり、それを補正するコードは発行されません。)

CPUにこのような誤った依存関係があるのはなぜですか?

私たちは推測することができます:それは同じ実行ユニットで実行されますbsf/ やる出力依存性を持っています。(POPCNTはどのようにハードウェアに実装されていますか?)これらの命令について、Intelはinput = 0の整数結果を「未定義」(ZF = 1として)として文書化しますが、実際には、Intelハードウェアは古いソフトウェアの破壊を回避するためのより強力な保証を提供します。AMDはこの動作を文書化しています。bsr

おそらく、この実行ユニットのいくつかのuopsを出力に依存させるのは不便でしたが、他のものはそうではありませんでした。

AMDプロセッサには、この誤った依存関係はありません。


完全なテストコードは参考のために以下にあります:

#include <iostream>
#include <chrono>
#include <x86intrin.h>

int main(int argc, char* argv[]) {

   using namespace std;
   uint64_t size=1<<20;

   uint64_t* buffer = new uint64_t[size/8];
   char* charbuffer=reinterpret_cast<char*>(buffer);
   for (unsigned i=0;i<size;++i) charbuffer[i]=rand()%256;

   uint64_t count,duration;
   chrono::time_point<chrono::system_clock> startP,endP;
   {
      uint64_t c0 = 0;
      uint64_t c1 = 0;
      uint64_t c2 = 0;
      uint64_t c3 = 0;
      startP = chrono::system_clock::now();
      for( unsigned k = 0; k < 10000; k++){
         for (uint64_t i=0;i<size/8;i+=4) {
            uint64_t r0 = buffer[i + 0];
            uint64_t r1 = buffer[i + 1];
            uint64_t r2 = buffer[i + 2];
            uint64_t r3 = buffer[i + 3];
            __asm__(
                "popcnt %4, %4  \n\t"
                "add %4, %0     \n\t"
                "popcnt %5, %5  \n\t"
                "add %5, %1     \n\t"
                "popcnt %6, %6  \n\t"
                "add %6, %2     \n\t"
                "popcnt %7, %7  \n\t"
                "add %7, %3     \n\t"
                : "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
                : "r"  (r0), "r"  (r1), "r"  (r2), "r"  (r3)
            );
         }
      }
      count = c0 + c1 + c2 + c3;
      endP = chrono::system_clock::now();
      duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "No Chain\t" << count << '\t' << (duration/1.0E9) << " sec \t"
            << (10000.0*size)/(duration) << " GB/s" << endl;
   }
   {
      uint64_t c0 = 0;
      uint64_t c1 = 0;
      uint64_t c2 = 0;
      uint64_t c3 = 0;
      startP = chrono::system_clock::now();
      for( unsigned k = 0; k < 10000; k++){
         for (uint64_t i=0;i<size/8;i+=4) {
            uint64_t r0 = buffer[i + 0];
            uint64_t r1 = buffer[i + 1];
            uint64_t r2 = buffer[i + 2];
            uint64_t r3 = buffer[i + 3];
            __asm__(
                "popcnt %4, %%rax   \n\t"
                "add %%rax, %0      \n\t"
                "popcnt %5, %%rax   \n\t"
                "add %%rax, %1      \n\t"
                "popcnt %6, %%rax   \n\t"
                "add %%rax, %2      \n\t"
                "popcnt %7, %%rax   \n\t"
                "add %%rax, %3      \n\t"
                : "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
                : "r"  (r0), "r"  (r1), "r"  (r2), "r"  (r3)
                : "rax"
            );
         }
      }
      count = c0 + c1 + c2 + c3;
      endP = chrono::system_clock::now();
      duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "Chain 4   \t"  << count << '\t' << (duration/1.0E9) << " sec \t"
            << (10000.0*size)/(duration) << " GB/s" << endl;
   }
   {
      uint64_t c0 = 0;
      uint64_t c1 = 0;
      uint64_t c2 = 0;
      uint64_t c3 = 0;
      startP = chrono::system_clock::now();
      for( unsigned k = 0; k < 10000; k++){
         for (uint64_t i=0;i<size/8;i+=4) {
            uint64_t r0 = buffer[i + 0];
            uint64_t r1 = buffer[i + 1];
            uint64_t r2 = buffer[i + 2];
            uint64_t r3 = buffer[i + 3];
            __asm__(
                "xor %%rax, %%rax   \n\t"   // <--- Break the chain.
                "popcnt %4, %%rax   \n\t"
                "add %%rax, %0      \n\t"
                "popcnt %5, %%rax   \n\t"
                "add %%rax, %1      \n\t"
                "popcnt %6, %%rax   \n\t"
                "add %%rax, %2      \n\t"
                "popcnt %7, %%rax   \n\t"
                "add %%rax, %3      \n\t"
                : "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
                : "r"  (r0), "r"  (r1), "r"  (r2), "r"  (r3)
                : "rax"
            );
         }
      }
      count = c0 + c1 + c2 + c3;
      endP = chrono::system_clock::now();
      duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "Broken Chain\t"  << count << '\t' << (duration/1.0E9) << " sec \t"
            << (10000.0*size)/(duration) << " GB/s" << endl;
   }

   free(charbuffer);
}

均等に興味深いベンチマークはここで見つけることができます:http://pastebin.com/kbzgL8si
このベンチマークは、多数の異なるpopcnt(偽)依存チェーンにあるのを。

False Chain 0:  41959360000 0.57748 sec     18.1578 GB/s
False Chain 1:  41959360000 0.585398 sec    17.9122 GB/s
False Chain 2:  41959360000 0.645483 sec    16.2448 GB/s
False Chain 3:  41959360000 0.929718 sec    11.2784 GB/s
False Chain 4:  41959360000 1.23572 sec     8.48557 GB/s

3
こんにちは!ここには過去のコメントがたくさんあります。新しいものを残す前に、アーカイブ確認してください。
Shog9 16

1
@ JustinL.itは、この特定の問題が7.0の時点でClangで修正されているようです
Dan M.

@PeterCordesスケジューラほど実行ユニットではないと思います。依存関係を追跡するのはスケジューラです。そしてそれを行うために、命令はいくつかの「命令クラス」にグループ化され、それぞれがスケジューラによってまったく同じように扱われます。したがって、3サイクルの「slow-int」命令はすべて、命令のスケジューリングのために同じ「クラス」に投入されました。
Mysticial

@Mysticial:あなたはまだそう思っていますか?それはもっともらしい imul dst, src, immことですが、出力依存性がなく、どちらも遅くはありませんlea。どちらも行いませんがpdep、2つの入力オペランドでエンコードされたVEXです。誤った依存を引き起こすのは実行ユニット自体ではないことに同意します。アーキテクチャレジスタオペランドの名前を物理レジスタに変更するのは、RATおよび発行/名前変更の段階です。おそらく、uop-code->依存パターンとポートの選択のテーブルが必要であり、同じ実行ユニットのすべてのuopsをグループ化すると、そのテーブルが単純化されます。それが私がより詳細に意味したことです。
Peter Cordes

それを編集して回答にしたいのか、それともスケジューラについて最初に言ったようなことを言い直すのかを教えてください。SKLがlzcnt / tzcntのfalse depを削除したがpopcntは削除しなかったという事実は、何かを教えてくれるはずですが、IDKはそれを教えてくれます。名前変更/ RAT関連のもう1つの考えられる兆候は、SKLがlzcnt / tzcntのメモリソースとしてpopcntではなく、インデックス付きアドレッシングモードのラミネートを解除することです。もちろん、名前変更ユニットは、バックエンドが表すことができるuopsを作成する必要があります。
Peter Cordes

50

同等のCプログラムをコーディングして実験しましたが、この奇妙な動作を確認できました。さらに、使用するとgccが64ビットuintを使用するようになるため、64 gccビット整数(size_tとにかくおそらくそれはおそらく)の方が優れているuint_fast32_tと考えています。

私はアセンブリを
少しいじくり回しました:単に32ビットバージョンを取り、プログラムの内部popcountループですべての32ビット命令/レジスタを64ビットバージョンに置き換えます。観察:コードは32ビットバージョン同じくらい高速です!

変数のサイズは実際には64ビットではないため、これは明らかにハックです。プログラムの他の部分はまだ32ビットバージョンを使用していますが、内部のpopcountループがパフォーマンスを支配している限り、これは良いスタートです。 。

次に、プログラムの32ビットバージョンから内部ループコードをコピーし、64ビットになるようにハッキングし、レジスタをいじって64ビットバージョンの内部ループの代わりにしています。このコードは、32ビットバージョンと同じ速度で実行されます。

私の結論は、これはコンパイラによる不適切な命令スケジューリングであり、32ビット命令の実際の速度/待ち時間の利点ではないということです。

(注意:アセンブリをハッキングしたので、気付かずに何かが壊れている可能性があります。そうは思いません。)


1
「さらに、gccは、uint_fast32_tを使用するとgccが64ビットuintを使用するため、64ビット整数[…]の方が優れていると考えています。」残念ながら、そして残念なことに、これらの型の背後に魔法や深いコードイントロスペクションはありません。プラットフォーム全体で可能なすべての場所とすべてのプログラムに対して単一のtypedefとして提供される以外の方法でそれらが提供されるのを見たことはまだありません。型の正確な選択の背後にはかなりの考えが置かれている可能性がありますが、それらのそれぞれの1つの定義は、これまでに存在するすべてのアプリケーションにおそらく適合しません。さらに読む:stackoverflow.com/q/4116297
Keno

2
@Kenoそれsizeof(uint_fast32_t)は定義されなければならないからです。それを許可しない場合、その策略を実行できますが、これはコンパイラー拡張機能を使用しないと実行できません。
wizzwizz4

25

これは答えではありませんが、結果をコメントにすると読みにくいです。

これらの結果はMac ProWestmere 6コアXeon 3.33 GHz)で得られます。私はそれをコンパイルしましたclang -O3 -msse4 -lstdc++ a.cpp -o a(-O2は同じ結果を得ます)。

クラング uint64_t size=atol(argv[1])<<20;

unsigned    41950110000 0.811198 sec    12.9263 GB/s
uint64_t    41950110000 0.622884 sec    16.8342 GB/s

クラング uint64_t size=1<<20;

unsigned    41950110000 0.623406 sec    16.8201 GB/s
uint64_t    41950110000 0.623685 sec    16.8126 GB/s

私も試しました:

  1. テストの順序を逆にすると、結果は同じになるため、キャッシュファクターが除外されます。
  2. 持ってfor逆に声明を:for (uint64_t i=size/8;i>0;i-=4)。これにより、同じ結果が得られ、コンパイルが(予想どおり)反復ごとにサイズを8で除算しないように十分にスマートであることが証明されます。

ここに私の野生の推測があります:

速度係数は3つの部分に分かれています。

  • コードキャッシュ:uint64_tバージョンのコードサイズは大きいですが、これは私のXeon CPUには影響しません。これにより、64ビットバージョンが遅くなります。

  • 使用される命令。ループカウントだけでなく、2つのバージョンの32ビットと64ビットのインデックスでバッファにアクセスすることに注意してください。64ビットオフセットでポインターにアクセスすると、専用の64ビットレジスターとアドレッシングが要求されますが、32ビットオフセットには即時を使用できます。これにより、32ビットバージョンが高速になる場合があります。

  • 命令は、64ビットコンパイル(つまり、プリフェッチ)でのみ発行されます。これにより、64ビットが高速になります。

3つの要因は、観察された一見矛盾する結果と一致します。


4
興味深いことに、コンパイラーのバージョンとコンパイラー・フラグを追加できますか?最良のことは、あなたのマシンでは、結果が好転することです。つまり、u64を使用する方が高速です。これまでは、ループ変数のタイプを考えたことはありませんでしたが、次回は2度考えなければならないようです:)。
2014

2
@gexicide:16.8201から16.8126へのジャンプを「より高速」にすることはしません。
user541686

2
@Mehrdad:私は意味ジャンプの間に1つある12.916.8、とてもunsigned速いここにあります。私のベンチマークでは、反対がunsigneduint64_t
当てはまり

@gexicideバッファー[i]のアドレスの違いに気付きましたか?
マスクできない割り込み

@カルビン:いいえ、どういう意味ですか?
2014

10

信頼できる回答はできませんが、考えられる原因の概要を説明します。このリファレンスは、ループの本体の命令について、レイテンシとスループットの比率が3:1であることを明確に示しています。また、複数のディスパッチの影響も示しています。最新のx86プロセッサには(ギブオアテイク)3つの整数ユニットがあるため、一般的にサイクルごとに3つの命令をディスパッチすることが可能です。

したがって、ピークパイプラインと複数のディスパッチパフォーマンス、およびこれらのメカニズムの障害の間では、パフォーマンスは6倍になります。x86命令セットの複雑さにより、風変わりな破損が発生しやすくなることはよく知られています。上記のドキュメントには良い例があります:

64ビットの右シフトに対するPentium 4のパフォーマンスは本当に悪い。64ビットの左シフトおよびすべての32ビットのシフトは、許容できるパフォーマンスを備えています。ALUの上位32ビットから下位32ビットへのデータパスが適切に設計されていないようです。

個人的には、4コアチップの特定のコア(思い出すとAMD)でホットループがかなり遅くなるという奇妙なケースに遭遇しました。実際には、コアをオフにすることでmap-reduce計算のパフォーマンスが向上しました。

ここで私が推測しているのは、整数ユニットの競合ですpopcnt。32ビット幅のカウンターでは、ループカウンターとアドレスの計算はすべてフルスピードでやっと実行できますが、64ビットカウンターは競合とパイプラインのストールを引き起こします。ループ本体の実行ごとに合計で約12サイクル、おそらく複数のディスパッチを伴う4サイクルしかないため、1つのストールが実行時間に2倍の影響を与える可能性があります。

静的変数を使用して引き起こされた変更は、命令のマイナーな並べ替えを引き起こすだけだと思いますが、32ビットコードが競合の転換点にあるという別の手がかりです。

これは厳密な分析ではありません、もっともらしい説明です。


2
残念ながら(Core 2?)以来、このコードには存在しない乗算/除算を除いて、32ビットと64ビットの整数演算には実質的にパフォーマンスの違いはありません。
Mysticial 2014

@Gene:すべてのバージョンがサイズをレジスターに保管し、ループ内のスタックからそれを決して読み取らないことに注意してください。したがって、少なくともループ内ではなく、アドレス計算を混在させることはできません。
2014

@ジーン:確かに興味深い説明!ただし、主なWTFポイントについては説明していません。パイプラインのストールが原因で64ビットが32ビットよりも遅いということは、1つのことです。しかし、これが事実である場合、64ビットバージョンは32 ビットバージョンよりも確実に遅いのではないでしょうか。代わりに、コンパイル時定数のバッファーサイズを使用すると、32ビットバージョンでも3つの異なるコンパイラーが遅いコードを生成します。バッファーサイズを静的に変更すると、完全に変更されます。私の同僚のマシン(およびCalvinの答え)でも、64ビットバージョンがかなり高速であるケースがありました。それは絶対に予測不可能であるように思われます
。–殺人剤

@Mysticialそれが私のポイントです。IU、バス時間などの競合がない場合、ピーク時のパフォーマンスの違いはありません。リファレンスはそれを明確に示しています。競合によりすべてが異なります。インテルコアの文献の例を次に示します。「デザインに含まれる1つの新しいテクノロジーは、2つのx86命令を単一のマイクロオペレーションに結合するMacro-Ops Fusionです。たとえば、比較の後に条件ジャンプが続くような一般的なコードシーケンス残念ながら、このテクノロジーは64ビットモードでは機能しません。」したがって、実行速度は2:1の比率になります。
Gene

@gexicide私はあなたが言っていることを理解していますが、あなたは私が意図した以上のものを推測しています。最も速く実行されているコードは、パイプラインとディスパッチキューをいっぱいに保つことです。この状態は脆弱です。合計データフローに32ビットを追加するような小さな変更と命令の並べ替えで十分です。要するに、操作とテストが前進する唯一の方法であるというOPアサーションは正しいです。
ジーン、

10

インデックスの代わりにポインターを使用してVisual Studio 2013 Expressでこれを試しましたが、プロセスが少し高速になりました。これは、アドレス指定がオフセット+レジスタ+(レジスタ<< 3)ではなく、オフセット+レジスタであるためと考えられます。C ++コード。

   uint64_t* bfrend = buffer+(size/8);
   uint64_t* bfrptr;

// ...

   {
      startP = chrono::system_clock::now();
      count = 0;
      for (unsigned k = 0; k < 10000; k++){
         // Tight unrolled loop with uint64_t
         for (bfrptr = buffer; bfrptr < bfrend;){
            count += __popcnt64(*bfrptr++);
            count += __popcnt64(*bfrptr++);
            count += __popcnt64(*bfrptr++);
            count += __popcnt64(*bfrptr++);
         }
      }
      endP = chrono::system_clock::now();
      duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "uint64_t\t"  << count << '\t' << (duration/1.0E9) << " sec \t"
           << (10000.0*size)/(duration) << " GB/s" << endl;
   }

アセンブリコード:r10 = bfrptr、r15 = bfrend、rsi = count、rdi = buffer、r13 = k:

$LL5@main:
        mov     r10, rdi
        cmp     rdi, r15
        jae     SHORT $LN4@main
        npad    4
$LL2@main:
        mov     rax, QWORD PTR [r10+24]
        mov     rcx, QWORD PTR [r10+16]
        mov     r8, QWORD PTR [r10+8]
        mov     r9, QWORD PTR [r10]
        popcnt  rdx, rax
        popcnt  rax, rcx
        add     rdx, rax
        popcnt  rax, r8
        add     r10, 32
        add     rdx, rax
        popcnt  rax, r9
        add     rsi, rax
        add     rsi, rdx
        cmp     r10, r15
        jb      SHORT $LL2@main
$LN4@main:
        dec     r13
        jne     SHORT $LL5@main

9

-funroll-loops -fprefetch-loop-arraysGCCに渡してみましたか?

これらの追加の最適化により、次の結果が得られます。

[1829] /tmp/so_25078285 $ cat /proc/cpuinfo |grep CPU|head -n1
model name      : Intel(R) Core(TM) i3-3225 CPU @ 3.30GHz
[1829] /tmp/so_25078285 $ g++ --version|head -n1
g++ (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3

[1829] /tmp/so_25078285 $ g++ -O3 -march=native -std=c++11 test.cpp -o test_o3
[1829] /tmp/so_25078285 $ g++ -O3 -march=native -funroll-loops -fprefetch-loop-arrays -std=c++11     test.cpp -o test_o3_unroll_loops__and__prefetch_loop_arrays

[1829] /tmp/so_25078285 $ ./test_o3 1
unsigned        41959360000     0.595 sec       17.6231 GB/s
uint64_t        41959360000     0.898626 sec    11.6687 GB/s

[1829] /tmp/so_25078285 $ ./test_o3_unroll_loops__and__prefetch_loop_arrays 1
unsigned        41959360000     0.618222 sec    16.9612 GB/s
uint64_t        41959360000     0.407304 sec    25.7443 GB/s

3
しかし、それでも、アンロールによって誤った依存関係の主な問題が解決されないため、結果は完全に奇妙です(最初に署名が速くなり、次にuint64_tが速くなります)。
2014

7

削減ステップをループの外に移動してみましたか?現在、実際には必要のないデータ依存関係があります。

試してください:

  uint64_t subset_counts[4] = {};
  for( unsigned k = 0; k < 10000; k++){
     // Tight unrolled loop with unsigned
     unsigned i=0;
     while (i < size/8) {
        subset_counts[0] += _mm_popcnt_u64(buffer[i]);
        subset_counts[1] += _mm_popcnt_u64(buffer[i+1]);
        subset_counts[2] += _mm_popcnt_u64(buffer[i+2]);
        subset_counts[3] += _mm_popcnt_u64(buffer[i+3]);
        i += 4;
     }
  }
  count = subset_counts[0] + subset_counts[1] + subset_counts[2] + subset_counts[3];

また、変なエイリアシングが行われていますが、厳密なエイリアシングルールに準拠しているかどうかはわかりません。


2
質問を読んだ後、それが最初にやったことです。依存チェーンを解除します。判明したように、パフォーマンスの違いは変わりません(少なくとも私のコンピューターでは-GCC 4.7.3のIntel Haswell)。
Nils Pipenbrinck 2014

1
@BenVoigt:厳密なエイリアシングに準拠しています。void*そしてchar*彼らはesentially「メモリの一部のチャンクへのポインタを」と考えていると、エイリアスすることができる2つのタイプがあり!データ依存関係の削除に関するあなたのアイデアは最適化には良いですが、質問には答えません。そして、@ NilsPipenbrinckが言うように、何も変更しないようです。
2014

@gexicide:厳密なエイリアシングルールは対称的ではありません。を使用char*してにアクセスできますT[]。安全にを使用してにアクセスすることはできず、コードは後者を実行しているように見えます。T*char[]
Ben Voigt 2014

@BenVoigt:mallocmallocが返ってvoid*それをと解釈するため、何も配列を保存することはできませんT[]。そして、私はそのことをかなり確信してvoid*おりchar*、厳密なエイリアシングに関して同じ意味論を持っていました。しかし、私はこれはここではかなり話題外だと思います:)
殺人剤

1
個人的には正しい方法だと思いますuint64_t* buffer = new uint64_t[size/8]; /* type is clearly uint64_t[] */ char* charbuffer=reinterpret_cast<char*>(buffer); /* aliasing a uint64_t[] with char* is safe */
Ben Voigt 2014

6

TL; DR:__builtin代わりに組み込み関数を使用してください。彼らはたまたま助けるかもしれません。

同じアセンブリ命令を使用することにより、gcc4.8.4(およびgcc.godbolt.orgの4.7.3)に最適なコードを生成させることができまし__builtin_popcountllたが、ラッキーになり、予期せずにコードを作成します誤った依存関係のバグが原因で、長いループ運搬依存関係。

ベンチマークコードについて100%確信はありませんが、objdump出力は私の見解を共有しているようです。他のいくつかのトリック(++ivs i++)を使用して、movl命令なしでコンパイラーにループを展開させます(奇妙な動作、私は言わなければなりません)。

結果:

Count: 20318230000  Elapsed: 0.411156 seconds   Speed: 25.503118 GB/s

ベンチマークコード:

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

uint64_t builtin_popcnt(const uint64_t* buf, size_t len){
  uint64_t cnt = 0;
  for(size_t i = 0; i < len; ++i){
    cnt += __builtin_popcountll(buf[i]);
  }
  return cnt;
}

int main(int argc, char** argv){
  if(argc != 2){
    printf("Usage: %s <buffer size in MB>\n", argv[0]);
    return -1;
  }
  uint64_t size = atol(argv[1]) << 20;
  uint64_t* buffer = (uint64_t*)malloc((size/8)*sizeof(*buffer));

  // Spoil copy-on-write memory allocation on *nix
  for (size_t i = 0; i < (size / 8); i++) {
    buffer[i] = random();
  }
  uint64_t count = 0;
  clock_t tic = clock();
  for(size_t i = 0; i < 10000; ++i){
    count += builtin_popcnt(buffer, size/8);
  }
  clock_t toc = clock();
  printf("Count: %lu\tElapsed: %f seconds\tSpeed: %f GB/s\n", count, (double)(toc - tic) / CLOCKS_PER_SEC, ((10000.0*size)/(((double)(toc - tic)*1e+9) / CLOCKS_PER_SEC)));
  return 0;
}

コンパイルオプション:

gcc --std=gnu99 -mpopcnt -O3 -funroll-loops -march=native bench.c -o bench

GCCバージョン:

gcc (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4

Linuxカーネルのバージョン:

3.19.0-58-generic

CPU情報:

processor   : 0
vendor_id   : GenuineIntel
cpu family  : 6
model       : 70
model name  : Intel(R) Core(TM) i7-4870HQ CPU @ 2.50 GHz
stepping    : 1
microcode   : 0xf
cpu MHz     : 2494.226
cache size  : 6144 KB
physical id : 0
siblings    : 1
core id     : 0
cpu cores   : 1
apicid      : 0
initial apicid  : 0
fpu     : yes
fpu_exception   : yes
cpuid level : 13
wp      : yes
flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx rdtscp lm constant_tsc nopl xtopology nonstop_tsc eagerfpu pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm arat pln pts dtherm fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 invpcid xsaveopt
bugs        :
bogomips    : 4988.45
clflush size    : 64
cache_alignment : 64
address sizes   : 36 bits physical, 48 bits virtual
power management:

3
'のfalse dep -funroll-loopsによって作成されたループキャリーの依存関係チェーンでボトルネックにならないコードを作成するのは、たまたま幸運ですpopcnt。誤った依存関係を認識していない古いコンパイラバージョンを使用すると、リスクが生じます。なければ-funroll-loops、GCC 4.8.5のループは、代わりにスループットのPOPCNT待ち時間にボトルネックになることはにカウントされるためrdxgcc 4.9.3によってコンパイルされた同じコードはxor edx,edx、依存関係チェーンを壊すためにを追加します。
Peter Cordes、2016年

3
古いコンパイラでは、コードはOPが経験したのとまったく同じパフォーマンス変動に対して脆弱です。一見些細な変更でも、問題が発生することがわからなかったため、gccが遅くなる可能性があります。 古いコンパイラで1つのケースでたまたま機能するものを見つけることは問題ではありません
Peter Cordes、2016年

2
記録のために、x86intrin.h_mm_popcnt_*GCCの機能が強制的にラッパーをインライン化しています__builtin_popcount*。インライン化により、一方が他方と完全に同等になるはずです。それらを切り替えることによって引き起こされる可能性のある違いを目にすることはないと思います。
ShadowRanger

-2

まず、ピークパフォーマンスを推定してみてください-https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdfを調べてください、特に付録C。

あなたのケースでは、POPCNT命令がレイテンシ= 3クロック、スループット= 1クロックであることを示しているのが表C-10です。スループットは、クロックでの最大レートを示します(可能な限り最高の帯域幅番号を取得するには、popcnt64の場合はコア周波数と8バイトを掛けます)。

次に、コンパイラーが行った処理を調べ、ループ内の他のすべての命令のスループットを合計します。これにより、生成されたコードの可能な限りの推定が得られます。

最後に、ループ内の命令間のデータ依存関係を確認します。スループットの代わりにレイテンシ-大きな遅延が発生するためです。データフローチェーンで単一反復の命令を分割し、それら全体のレイテンシを計算してから、それらから単純に最大値を取得します。データフローの依存関係を考慮して、おおまかな見積もりを出します。

ただし、あなたの場合、正しい方法でコードを書くだけで、これらの複雑さはすべて解消されます。同じcount変数に累積するのではなく、異なる変数(count0、count1、... count8など)に累積して、最後に合計します。または、counts [8]の配列を作成し、その要素に蓄積します。おそらく、それはベクトル化され、スループットが大幅に向上します。

PSでベンチマークを1秒間実行しないでください。最初にコアをウォームアップしてから、ループを少なくとも10秒以上100秒以上実行します。それ以外の場合は、電源管理ファームウェアとハ​​ードウェアでのDVFS実装をテストします。

PPSベンチマークを実際に実行するのにどのくらいの時間がかかるかについて、無限の議論があると聞きました。最も賢い人々は、なぜ11秒や12秒ではなく、なぜ10秒なのかさえ尋ねています。これは理論的には面白いと認めるべきです。実際には、ベンチマークを100回続けて実行し、偏差を記録するだけです。それはIS面白いです。ほとんどの人は、ソースを変更し、その後ベンチを実行して、新しいパフォーマンスレコードをキャプチャします。正しいことを正しく行います。

まだ納得できませんか?assp1r1n3(https://stackoverflow.com/a/37026212/9706746)によるベンチマークの上記のCバージョンを使用して、再試行ループで10000ではなく100を試してください。

私の7960Xは、RETRY = 100で示しています。

カウント:203182300経過:0.008385秒速度:12.505379 GB / s

カウント:203182300経過:0.011063秒速度:9.478225 GB / s

カウント:203182300経過:0.011188秒速度:9.372327 GB / s

カウント:203182300経過:0.010393秒速度:10.089252 GB / s

カウント:203182300経過:0.009076秒速度:11.553283 GB / s

RETRY = 10000の場合:

カウント:20318230000経過:0.661791秒速度:15.844519 GB /秒

カウント:20318230000経過:0.665422秒速度:15.758060 GB / s

カウント:20318230000経過:0.660983秒速度:15.863888 GB /秒

カウント:20318230000経過:0.665337秒速度:15.760073 GB / s

カウント:20318230000経過:0.662138秒速度:15.836215 GB / s

PPPS最後に、「受け入れられた答え」と他の謎について;-)

assp1r1n3の答えを使用してみましょう-彼は2.5Ghzコアを持っています。POPCNTには1クロックのスループットがあり、彼のコードは64ビットpopcntを使用しています。したがって、計算は2.5Ghz * 1クロック* 8バイト= 20 GB / sです。彼は25Gb / sを見ています、おそらく3Ghzへのターボブーストが原因です。

したがってark.intel.comに行き、i7-4870HQを探します。 https://ark.intel.com/products/83504/Intel-Core-i7-4870HQ-Processor-6M-Cache-up-to-3-70 -GHz-?q = i7-4870HQ

そのコアは最大3.7Ghzで動作し、実際の最大レートは彼のハードウェアで29.6 GB / sです。では、別の4GB / sはどこにあるのでしょうか?おそらく、それは各反復内のループロジックと他の周囲のコードに費やされています。

今、どこで、この偽の依存性は?ハードウェアはほぼピークレートで動作します。多分私の数学は悪いです、それは時々起こります:)

PPPPPSそれでもHWエラッタを示唆している人は犯人なので、私はその示唆に従い、インラインasmの例を作成しました。以下を参照してください。

私の7960Xでは、最初のバージョン(cnt0への単一出力)は11MB / sで実行され、2番目のバージョン(cnt0、cnt1、cnt2およびcnt3への出力)は33MB / sで実行されます。そして言うことができます-出来上がり!出力依存関係です。

OK、多分、私が言ったポイントは、このようなコードを書くのは意味がなく、出力依存関係の問題ではなく、ばかげたコード生成だということです。ハードウェアのテストは行っていません。最大限のパフォーマンスを引き出すコードを作成しています。HW OOOがそれらの「出力依存関係」の名前を変更して非表示にすることを期待できますが、ああ、正しいことを正しく行うだけで、謎に直面することは決してありません。

uint64_t builtin_popcnt1a(const uint64_t* buf, size_t len) 
{
    uint64_t cnt0, cnt1, cnt2, cnt3;
    cnt0 = cnt1 = cnt2 = cnt3 = 0;
    uint64_t val = buf[0];
    #if 0
        __asm__ __volatile__ (
            "1:\n\t"
            "popcnt %2, %1\n\t"
            "popcnt %2, %1\n\t"
            "popcnt %2, %1\n\t"
            "popcnt %2, %1\n\t"
            "subq $4, %0\n\t"
            "jnz 1b\n\t"
        : "+q" (len), "=q" (cnt0)
        : "q" (val)
        :
        );
    #else
        __asm__ __volatile__ (
            "1:\n\t"
            "popcnt %5, %1\n\t"
            "popcnt %5, %2\n\t"
            "popcnt %5, %3\n\t"
            "popcnt %5, %4\n\t"
            "subq $4, %0\n\t"
            "jnz 1b\n\t"
        : "+q" (len), "=q" (cnt0), "=q" (cnt1), "=q" (cnt2), "=q" (cnt3)
        : "q" (val)
        :
        );
    #endif
    return cnt0;
}

コアクロックサイクルで(秒ではなく)タイミングを計測している場合、小さなCPUにバインドされたループでは1秒で十分です。大きな違いを見つけたり、パフォーマンスカウンターでuopカウントを確認したりするには、100ミリ秒でも問題ありません。特に、Skylakeでは、ハードウェアのP状態管理により、ロード開始後、マイクロ秒単位で最大クロック速度までランプアップできます。
Peter Cordes

clangは__builtin_popcountlAVX2 vpshufbで自動ベクトル化でき、Cソースに複数のアキュムレータを必要としません。わからない_mm_popcnt_u64; AVX512-VPOPCNTでのみ自動ベクトル化される可能性があります。(AVX-512またはAVX-2 / を使用した大きなデータでの1ビット(人口カウント)のカウントを参照)
Peter Cordes

しかし、いずれにしても、インテルの最適化マニュアルを見ても役に立ちません。受け入れられた答えが示すように、問題はの予期しない出力依存関係ですpopcnt。これは、最近のマイクロアーキテクチャの一部に関するIntelのエラッタに記載されていますが、当時はそうではなかったと思います。予期しない誤った依存関係がある場合、dep-chain分析は失敗するため、この回答は良い一般的なアドバイスですが、ここでは適用されません。
Peter Cordes

1
私をからかってるの?手書きのasmループでパフォーマンスカウンターを使用して実験的に測定できるものを「信じる」必要はありません。それらは単なる事実です。私はテストしましたが、Skylakeはlzcnt/ の誤った依存関係を修正しましたが、は修正してtzcntいませんpopcnt。インテルのエラータSKL029参照してくださいintel.com/content/dam/www/public/us/en/documents/...を。また、gcc.gnu.org / bugzilla / show_bug.cgi?id = 62011は「解決済みの修正済み」であり、「無効」ではありません。HWに出力依存関係がないという主張の根拠はありません。
Peter Cordes

1
popcnt eax, edx/のような単純なループを作成すると、dec ecx / jnzクロックあたり1で実行され、popcntスループットと分岐ブランチスループットでボトルネックになることが予想されます。しかし、実際にはpopcnt書き込み専用であると予想されていても、EAXを繰り返し上書きするための待ち時間でボトルネックとなっている3クロックごとに1つだけで実行されます。Skylakeがあるので、自分で試すことができます。
Peter Cordes

-3

さて、私は、OPが尋ねたサブ質問の1つに対して、既存の質問では対処されていないように思われる小さな回答を提供したいと思います。警告、私はテストやコード生成、逆アセンブルを行っていないので、他の人が説明できるように考えを共有したかっただけです。

なぜstaticパフォーマンスが変わるのですか?

問題の行: uint64_t size = atol(argv[1])<<20;

簡潔な答え

アクセス用に生成されたアセンブリを見sizeて、非静的バージョンに関連するポインタの間接化の追加手順があるかどうかを確認します。

長い答え

宣言されているかどうかに関係なく、変数のコピーは1つしかstaticなく、サイズは変化しないので、違いは、変数をバックアップするために使用されるメモリの場所と、コード内で使用される場所との理論です。ダウン。

わかりやすいことから始めましょう。関数のすべてのローカル変数(およびパラメーター)は、ストレージとして使用するためにスタック上にスペースが提供されていることを思い出してください。さて、明らかに、main()のスタックフレームはクリーンアップされることはなく、一度だけ生成されます。OK、作ってみstaticませんか?まあ、その場合、コンパイラはプロセスのグローバルデータスペースにスペースを予約することを知っているので、スタックフレームを削除しても場所をクリアできません。しかし、それでも、1つの場所しかないので、違いは何ですか?スタック上のメモリ位置がどのように参照されるかに関係しているのではないかと思います。

コンパイラがシンボルテーブルを生成するとき、サイズなどの関連する属性とともにラベルのエントリを作成するだけです。メモリ内の適切なスペースを予約する必要があることを認識していますが、実際にはその位置を選択するのは、しばらくしてからです。活性分析を行った後にプロセスを処理し、おそらく割り当てを登録します。リンカは、最終的なアセンブリコードのマシンコードに提供するアドレスをどのようにして知るのでしょうか。それは最終的な場所を知っているか、その場所に到着する方法を知っています。スタックを使用すると、1つの2つの要素、つまりスタックフレームへのポインタとフレームへのオフセットに基づいて場所を参照するのは非常に簡単です。これは基本的に、リンカが実行前にスタックフレームの場所を認識できないためです。


2
OPがテストしているIntel CPUのstatic誤った出力依存性に影響を与える方法で、関数のレジスタ割り当てを変更するpopcntと、コンパイラがそれらを回避することを知らなかった可能性が高いようです。(インテルCPUのこのパフォーマンスポットホールはまだ発見されていなかったため)。コンパイラーはstatic、自動ストレージ変数と同様に、ローカル変数をレジスターに保持できますが、main実行が1回だけであると想定して最適化しないと、 code-gen(値は最初の呼び出しによってのみ設定されるため。)
Peter Cordes

1
とにかく、モード[RIP + rel32][rsp + 42]アドレッシングモードのパフォーマンスの違いは、ほとんどの場合、ごくわずかです。 cmp dword [RIP+rel32], immediate単一のload + cmp uopにマイクロヒューズすることはできませんが、それが要因になるとは思いません。先ほど述べたように、ループの内部ではおそらくとにかくレジスタにとどまりますが、C ++を微調整するとコンパイラの選択が異なる可能性があります。
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.