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 のランダム データ):
ご覧のとおり、uint64_t バージョンのスループットは署名なしバージョンの半分です。問題は、異なるアセンブリが生成されることのようですが、理由は何でしょうか?まず、コンパイラのバグだと思ったので、clang (Ubuntu Clang バージョン 3.4-1ubuntu3) を試してみました:
clang++ -O3 -march=native -std=c++11 teest.cpp -o test
テスト結果 1:
つまり、ほぼ同じ結果が得られますが、それでも奇妙です。しかし今では本当に奇妙になってしまいました。入力から読み取られたバッファ サイズを定数 1 に置き換えたので、
uint64_t size = atol(argv[1]) << 20;
から
uint64_t size = 1 << 20;
に変更しました。これにより、コンパイラはコンパイル時にバッファ サイズを認識できるようになりました。もしかしたら、いくつかの最適化を追加できるかもしれません。 g 単位の数値は次のとおりです:
どちらのバージョンも同等に高速になりました。ただし、velocidade は署名なしと比較してさらに遅くなります。 26 GB/秒から 20 GB/秒に低下したため、型破りな定数を定数値に置き換えると非最適化が発生しました。真剣に、ここでは手がかりがありません!しかし、Clang と新しいバージョンでは、
uint64_t size = atol(argv[1]) << 20;
が
uint64_t size = 1 << 20;
に変更されました。 結果:
待て、何が起こった?現在、どちらのバージョンも 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:
はい、別の代替手段もあります。 u3 ではまだ 32GB/s ですが、u64 では少なくとも 13GB/s バージョンから 20GB/s バージョンに到達することができました。同僚のコンピュータでは、u64 バージョンの方が u32 バージョンよりもさらに高速で、最良の結果が得られました。残念ながら、これは g でのみ機能し、clang は静的を気にしていないようです。
**私の質問
以上がループ カウンタを「unsigned」から「uint64_t」に変更すると、x86 CPU 上の「_mm_popcnt_u64」のパフォーマンスに大きな影響を与えるのはなぜですか。また、コンパイラの最適化と変数宣言はどのように影響しますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。