最も正確なコード測定



6か月前の長い算術に関する私の記事では、非常に短いコードフラグメントの速度(ティック単位のスループット)の測定値があります-ほんの数命令です。 測定手法は曲がっていましたが、もっともらしい結果が得られました。 その後、結果がまだ間違っていることが判明しました-表面的なアプローチが常に影響します。



この投稿では、エラーを最小限に抑え、特別なライブラリとドライバーを接続せずに「ナノベンチマーク」を行う信頼できる方法について説明します。 適用性:プロセッサのシングルスレッドの可能性の比較、興味のみ。



私はGCCのみを使用します-したがって、この方法は鋭くなります。 しかし、他のコンパイラの所有者が理解できるように、一般化を行います。




直接的な手段はRDTSCチームです。 ウィキペディアは、それが信頼できないことを正しく指摘しており、特別なOSサービスを使用することが推奨されています。 ただし、それらは微小測定(数百クロックサイクル)には長すぎ、開始から開始まで同じではないため、致命的なエラーが発生します。 RDTSC自体は、数十サイクル以上は動作しません-定数または小さなセットの1つです。



マイクロ計測におけるRDTSCの唯一の欠点は、浮動プロセッサクロック速度です。これは、タイムスタンプカウンターが常に標準の乗数に従ってクロックをカウントするためです。 乗数の修正は必ずしも簡単な作業ではありません; OS名とプロセッサタイプと組み合わせて「 CPUスケーリングを無効にする 」を探してください。 Gnomeでの良い解決策は、 indicator-cpufreq



アプレットです。



測定ハーネスは、3つのネストされたループで構成されています。



内側のループは、作業プログラムのようにデータの流れを制御します。

この精神で:
 type input1[n]; type input2[n]; type output[n]; ... for (int i = 0; i < n; i++) {  input1[i]  input2[i]    output[i] }
      
      





「コードを測定する」というフレーズで、この記事の「コード」は特定のプロセッサ命令のシーケンスを意味することが重要です。 したがって、括弧の間のループ部分は、アセンブラーまたはCで記述する必要がありますが、コンパイラーから得られるものを明確に理解する必要があります。 -O3



でGCCの必死のアクティビティを克服するには、すぐにオプション-fno-prefetch-loop-arrays



-fno-unroll-loops



-ftree-vectorizer-verbose=1



追加します。 -fno-tree-vectorize



または-ftree-vectorize



出力に必要なものに応じて-「そのまま」またはベクトル化されたループ。



特定の入力またはコードの処理を入力/出力なしで測定したい場合は、ループでラップします。 GCC -fno-gcse



コードを-fno-gcse



ないようにするには、 -fno-gcse



(グローバル共通部分式除去)、 -fno-gcse



-fno-tree-pta



(ポイントツー分析)、および-fno-tree-pre



(部分冗長除去)を有効にします。 すべての最適化オプションをご覧ください。



ループの先頭を32バイトに揃えます。 -falign-loops



-O2



)を使用すると、GCCは独自にこれを実行します。



中間サイクルには、2つの測定サイクルと、中間の一定の内部サイクルが含まれます。 その役割は、内部サイクルを完了することができる最小時間を決定することです。 すべてのデータをキャッシュに入れるには20〜30回の反復で十分です。開始と終了のRDTSCは同じ時間を要し、他のすべての星が存在する場合は一緒になります:-)



外側のループ内側のループ長さを制御します。 入力データの初期化を中間ループの前に配置します。



中間サイクルで到達する時間には常に定数が含まれるため、外部サイクルが必要です-内部サイクルの初期化時間+遷移を予測する際の1エラーのコスト(最もインテリジェントなIntelコアはミスを少なくします)。 したがって、中間サイクルからの時間を単純に反復回数で割ることはできません。



しかし、それだけではありません! 1回の反復で長さが異なる内部ループの実行時間の違いは、しばしば大きく異なります。 その理由は、コンベアのさまざまな段階が相互に影響するためです。 ある段階で、アイデアが機能するようになったときに、次のことが実際に発生する可能性があります。

さらに、移行予測子やマイクロオペレーションスケジューラなど、一部のステップには通常とは異なる動作があります。 これはすべて、複雑な効果をもたらします。



その結果、1〜10-15(?)の長さの「パターン」の反復がコンベアに設定されます。



1回の反復ではなく、少なくとも1つのパターンについて、正確なスループットをティックでカウントすることは理にかなっています。



以下の例の数字から簡単にわかるように、パターンを測定する場合でも、結果のばらつきが残ります。 おそらくRDTSCは実際には上記のものほど良くありません:-)



そのため、パターンのステップの倍数である長さを持つ内部サイクルの実行時間の違いを受け取ったので、統計を計算する必要があります。





長い演算に関する記事の測定結果(以下、メジャーのすべての値)を比較します。

表面法 7.5 5.5 5.5 7 5 2 2.5 3.25(?)-3.5
スマートな方法 7 6 6 7 5 2 2 3


それ以降のすべてのテストは、 AMD K10Intel Core 2 Wolfdaleの 2つのコアで実行されました。



ツール自体を評価することが重要です。



空のループ
内側のループは次のようになります。

 for (int i = 0; i < inner_len; i++) { asm volatile ( "" ); }
      
      





 K10 1.8±0.7
コア2 10.0±2.4、10回の反復。
さらに(10、1.0)-(パターンの長さ、合計1回の反復)



RDTSC
中間および内部サイクルなし:

 typedef unsigned long long ull; inline ull rdtsc() { unsigned int lo, hi; asm volatile ( "rdtsc\n" : "=a" (lo), "=d" (hi) ); return ((ull)hi << 32) | lo; } ... for (int i = 0; i < TOTAL_VALUES; i++) { ull t1 = rdtsc(); ull t2 = rdtsc(); printf("%lld\n", t2 - t1); }
      
      





 K10 69.7±1.5
コア2 31.0±0.3




近似正弦計算



テイラーの3次の次の正弦を計算することでどれだけ節約できるかを見るのは興味深いです。 −π / 2からπ/ 2の角度では、小数点以下2桁の精度が得られます。 十分なアプリケーションを想像できます。



フレーム:

 #include <cstdio> #include <cstdlib> #include <cmath> #include <limits> typedef unsigned long long ull; #define MIDDLE_LEN (20) #define TOTAL_VALUES (10000) #define VEC_LEN (1) // len in _numbers_ #define DATA_LEN (TOTAL_VALUES * VEC_LEN) inline ull rdtsc() { unsigned int lo, hi; asm volatile ( "rdtsc\n" : "=a" (lo), "=d" (hi) ); return ((ull)hi << 32) | lo; } typedef double my_float; #define BYTE_LEN (DATA_LEN * sizeof(my_float)) int main() { my_float *angles = (my_float *) malloc(BYTE_LEN); my_float *sines = (my_float *) malloc(BYTE_LEN);   for (int inner_len = 0; inner_len < DATA_LEN; inner_len += VEC_LEN) { for (int i = 0; i < inner_len; i++)  angles[i] ull inner_min = std::numeric_limits<ull>::max(); for (int mi = 0; mi < MIDDLE_LEN; mi++) { ull t1 = rdtsc(); for (int i = 0; i < inner_len; i += VEC_LEN) {   angles[i]  sines[i] } ull t = rdtsc() - t1; inner_min = t < inner_min ? t : inner_min; } //     printf("%lld\n", inner_min); } }
      
      







FSIN命令-正確なサイン
math.h



からsin



によって呼び出されるのは彼女です。 実行速度も角度に依存するため、生成されたマイクロオペレーションはおそらくこのサインの実装に似ています。 したがって、ループ内で同じ角度の正弦が計算される場合、正確なスループットが意味をなします。 角度に依存しない大まかな計算と比較するには、ランダムな角度の平均が必要です。



 //  my_float randoms[DATA_LEN]; for (int i = 0; i < DATA_LEN; i++) randoms[i] = rand() / 2.0 / RAND_MAX * M_PI; //   angles[i] = 0.0  0.0001  M_PI * 0.5  randoms[i]; //   asm volatile ( "fldl (%0)\n\t" "fsin\n\t" "fstpl (%1)\n\t" :: "r" (angles + i), "r" (sines + i) );
      
      



角度 0.0 0.0001 π/ 2 ランダム
K10 30.2±10.3 89.8±2.9 143.1±8.5(2、71.6) 75.6
コア2 40.0±11.0 68.0±5.6 88.0±13.0 89.4




3次のテイラー級数
 //  my_float d6 = 1.0 / 6.0; my_float d120 = 1.0 / 120.0; my_float randoms[DATA_LEN]; for (int i = 0; i < DATA_LEN; i++) randoms[i] = rand() / 2.0 / RAND_MAX * M_PI; //   angles[i] = randoms[i]; //   my_float x = angles[i]; sines[i] = x - x*x*x*d6 + x*x*x*x*x*d120;
      
      



K10 61.2±15.6( 8、7.7
コア2 35.2±16.8( 4、8.8


ベクトル化されたテイラー級数
GCC -ftree-vectorize



オプションを追加するだけで、基本的に同じ結果になります(上記を参照)。 そして、ここではベクター拡張が使用されます。

 #define VEC_LEN (2) typedef my_float float_vector __attribute__ ((vector_size (16))); ... //  float_vector d6_v = {1.0 / 6.0, 1.0 / 6.0}; float_vector d120_v = {1.0 / 120.0, 1.0 / 120.0}; my_float randoms[DATA_LEN]; for (int i = 0; i < DATA_LEN; i++) randoms[i] = rand() / 2.0 / RAND_MAX * M_PI; //   angles[i] = randoms[i]; //   float_vector x = *((float_vector *)(angles + i)); *((float_vector *)(sines + i)) = x - x*x*x*d6_v + x*x*x*x*x*d120_v;
      
      



K10 41.8±14.2(1サインあたり5、8.4、4.2)
コア2 44.3±16.6(5、8.9、4.5)
1反復の速度はスカラーバージョンよりもわずかに遅く、1角度のサインの計算はほぼ2倍速くなります。



2文字の精度でのサインの計算は、通常よりも少なくとも10倍速く編成できることがわかりました。





ソース



その他のリンク



追伸

説明されているメソッド 、コードをプロファイルしません 。 パフォーマンスがコンピューティングパイプラインにかかっていても、ソリューションを通常のクロック()-100万回ごとのサイクルと常に比較できるため、最適化に役立つ可能性は非常に小さいです。



All Articles