ホームページ > バックエンド開発 > C++ > ループ カウンタを「unsigned」から「uint64_t」に変更すると、x86 CPU 上の「_mm_popcnt_u64」のパフォーマンスに大きな影響を与えるのはなぜですか。また、コンパイラの最適化と変数宣言はどのように影響しますか?

ループ カウンタを「unsigned」から「uint64_t」に変更すると、x86 CPU 上の「_mm_popcnt_u64」のパフォーマンスに大きな影響を与えるのはなぜですか。また、コンパイラの最適化と変数宣言はどのように影響しますか?

Linda Hamilton
リリース: 2024-12-05 10:42:15
オリジナル
890 人が閲覧しました

Why does changing a loop counter from `unsigned` to `uint64_t` significantly impact the performance of `_mm_popcnt_u64` on x86 CPUs, and how does compiler optimization and variable declaration affect this performance difference?

u64 ループ カウンタと x86 CPU 上の _mm_popcnt_u64 の間の異常なパフォーマンスの違いを探る

はじめに

大規模なデータ配列に対する操作を簡単に実行する方法を探していますPopcount メソッドを実行すると、非常に奇妙な動作が発生しました。ループ変数を unsigned から uint64_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 MB のランダム データ バッファーを作成しました。ここで、x はコマンド ラインから読み取られます。次に、バッファーを反復処理し、x86 ポップカウント組み込み関数のアンロール バージョンを使用してポップカウントを実行します。より正確な結果を得るために、ポップカウントを 10,000 回実行します。ポップカウントを測定する時間。最初のケースでは、内部ループ変数は符号なしであり、2 番目のケースでは、内部ループ変数は 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.50GHz でテストを実行しました。 1 の結果 (つまり 1MB のランダム データ):

  • unsigned 41959360000 0.401554 秒 26.113 GB/秒
  • uint64_t 41959360000 0.759822 秒 13.8003 GB/秒

ご覧のとおり、uint64_t バージョンのスループットは署名なしバージョンの半分です。問題は、異なるアセンブリが生成されることのようですが、理由は何でしょうか?まず、コンパイラのバグだと思ったので、clang (Ubuntu Clang バージョン 3.4-1ubuntu3) を試してみました:

clang++ -O3 -march=native -std=c++11 teest.cpp -o test
ログイン後にコピー

テスト結果 1:

  • unsigned 41959360000 0.398293 秒 26.3267 GB/秒
  • uint64_t 41959360000 0.680954 秒 15.3986 GB/秒

つまり、ほぼ同じ結果が得られますが、それでも奇妙です。しかし今では本当に奇妙になってしまいました。入力から読み取られたバッファ サイズを定数 1 に置き換えたので、

uint64_t size = atol(argv[1]) << 20;
ログイン後にコピー
ログイン後にコピー

から

uint64_t size = 1 << 20;
ログイン後にコピー
ログイン後にコピー

に変更しました。これにより、コンパイラはコンパイル時にバッファ サイズを認識できるようになりました。もしかしたら、いくつかの最適化を追加できるかもしれません。 g 単位の数値は次のとおりです:

  • unsigned 41959360000 0.509156 秒 20.5944 GB/秒
  • uint64_t 41959360000 0.508673 秒 20.6139 GB/秒

どちらのバージョンも同等に高速になりました。ただし、velocidade は署名なしと比較してさらに遅くなります。 26 GB/秒から 20 GB/秒に低下したため、型破りな定数を定数値に置き換えると非最適化が発生しました。真剣に、ここでは手がかりがありません!しかし、Clang と新しいバージョンでは、

uint64_t size = atol(argv[1]) << 20;
ログイン後にコピー
ログイン後にコピー

uint64_t size = 1 << 20;
ログイン後にコピー
ログイン後にコピー

に変更されました。 結果:

  • 符号なし 41959360000 0.677009 秒 15.4884 GB/秒
  • uint64_t 41959360000 0.676909 秒 15.4906 GB/秒

待て、何が起こった?現在、どちらのバージョンも 15GB/s の低速速度に低下しています。そのため、型破りな定数値を定数値に置き換えると、Clang のコードの 2 つ のバージョンが遅くなることさえありました。

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

もっとクレイジーにしてください!

最初の例 (atol(argv[1]) を使用した例) を使用して、変数の前に static を置きます。つまり、

#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);
}
ログイン後にコピー
ログイン後にコピー

これが彼女の内容です。結果は g:

  • unsigned 41959360000 0.396728 秒 26.4306 GB/秒
  • uint64_t 41959360000 0.509484 秒 20.5811 GB/秒

はい、別の代替手段もあります。 u3 ではまだ 32GB/s ですが、u64 では少なくとも 13GB/s バージョンから 20GB/s バージョンに到達することができました。同僚のコンピュータでは、u64 バージョンの方が u32 バージョンよりもさらに高速で、最良の結果が得られました。残念ながら、これは g でのみ機能し、clang は静的を気にしていないようです。

**私の質問

以上がループ カウンタを「unsigned」から「uint64_t」に変更すると、x86 CPU 上の「_mm_popcnt_u64」のパフォーマンスに大きな影響を与えるのはなぜですか。また、コンパイラの最適化と変数宣言はどのように影響しますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート