C ++でのプログラムのランタイムの最適化(条件付き遷移を削除)

LDPCデコーダーを使用してアルゴリズムの実行時間を最適化するとき、プロファイラーは次の値を計算する関数を導きました。

画像

ここabは整数です。 呼び出しの数は数百万に達し、その実装は十分でした シンプルで洗練されていない:



int LLR(int a, int b) { if (a>0) return (b>0) ? __min(a,b) : -__min(a,-b); else return (b>0) ? -__min(-a,b) : __min(-a,-b); }
      
      







関数は、基本的に3つの順次比較操作で構成されます。 これにより、(コンパイラーの最適化を考慮して)結果を得るために2(異なる符号の数の場合)または3(1の場合)の条件付き遷移が得られます。 多数の条件付き遷移を伴う組立ライン潜在的な問題を想起し、それらの数を減らすか、それらを取り除くことさえ決定されました。 パフォーマンスを評価するには、小さな
テストプロジェクト
 #include <Windows.h> static inline int LLR(int a, int b) { if (a>0) return (b>0) ? __min(a,b) : -__min(a,-b); else return (b>0) ? -__min(-a,b) : __min(-a,-b); } int _tmain(int argc, _TCHAR* argv[]) { SetPriorityClass(GetCurrentProcess(),REALTIME_PRIORITY_CLASS); SetThreadPriority(GetCurrentThread(),THREAD_PRIORITY_TIME_CRITICAL); srand(0); int x(0); __int64 t1,t2; QueryPerformanceCounter((LARGE_INTEGER*)&t1); for (size_t i=0;i<MAXUINT>>4;i++) { int a = rand() - RAND_MAX / 2; int b = rand() - RAND_MAX / 2; /* x += LLR(a,b);*/ } QueryPerformanceCounter((LARGE_INTEGER*)&t2); t2 -= t1; QueryPerformanceFrequency((LARGE_INTEGER*)&t1); _tprintf_s(_T("%f"),t2/(t1*1.)); return 0; }
      
      







アセンブリは、x86プラットフォームのリリース構成(デフォルト設定)でMSVS 2008で実行されました。



まず、計算関数の呼び出しをコメントアウトして、乱数の生成を伴うサイクルの実行時間を推定します(残念ながら、 QueryPerformanceCounter()の呼び出し自体はかなり遅く、サイクル内で行われた場合、結果の大幅な歪みにつながります)。 結果が再現可能であることを確認するために、プロジェクトを数回起動します。 3.4 GHzの周波数のIntel Core i7-3770プロセッサーを搭載したマシンでは、平均実行時間は9.2秒です。 計算関数の呼び出しをコメント解除し、プロジェクトを再構築して実験を繰り返すと、約11.5秒かかります。 実行時間の増加は明らかであり、私たちはそれに対処します。



条件文を削除してみましょう。 まず、製品abの符号を見つけます。 額の計算は、オーバーフローの可能性があるため、正しくありません。 符号のみが重要であるため(つまり、整数の符号桁のビットの値)、 xor演算を使用して積abの符号を決定することが適切です。 次に、結果a xor bを31ビットだけ右に移動し(符号付き数字のみを残す)、負でない数の場合は0を、負の場合は1を取得します。 この値に2を掛け、1から減算し、負の数の場合は-1、非負の場合は1を取得します。

 int sign = 1-2*((unsigned)a^b)>>31);
      
      



ここで、モジュールaおよびbを計算します。 同様の方法で、符号係数aおよびbを決定し、それらを乗算します。

 int c; c = 1-2*(((unsigned)a)>>31); a *= c; c = 1-2*(((unsigned)b)>>31); b *= c;
      
      



最小値の計算に進みます。 abにはすでに非負の値があるため、最小値はそれらの差の符号に基づいて計算できます。

 int numbers[2], min; numbers[0] = b; numbers[1] = a; a -= b; c = (unsigned(a))>>31; min = numbers[c];
      
      



すべてをまとめて取得する
次の機能
 static inline int LLR_2(int a, int b) { int sign, numbers[2]; sign = 1-2*(((unsigned)a^b)>>31); a *= 1-2*(((unsigned)a)>>31); b *= 1-2*(((unsigned)b)>>31); numbers[0] = b; numbers[1] = a; a -= b; return sign*numbers[((unsigned)a)>>31]; }
      
      







LLR()呼び出しをLLR_2()に置き換えて、何が起こるかを確認します。 現在、アセンブラコードには単一の条件付きジャンプはありませんが、整数乗算imulには3つの命令があります(2を乗算すると、コンパイラ自体がシフトに置き換えられます)。 テストプログラムを実行すると、次の結果が得られます。実行時間は9.4秒です。 合計で、目標値を計算するための2つのオプションの「正味」時間(それぞれ2.1秒と0.2秒)を比較すると、必要な操作の速度が10倍*向上します。



もう少し進んでみましょう。 整数乗算は、数値の符号を反転するためにのみ必要です。これは直接実行できます。 特に、 aのモジュラスの計算は、次のように実装できます。

 unsigned int mask[] = {0,(unsigned)-1}; unsigned int constant[] = {0, 1}; int c = ((unsigned)a)>>31; a = a^mask[c]+constant[c];
      
      



最後に、 _rotl()関数を使用して、31ビットの右シフトを単一の循環左シフトに置き換えます。 今後、コンパイラーはcallを使用せずに呼び出しを直接rolステートメントに変換することに注意してください。 再びすべてをまとめて取得する
3番目のオプション
 static unsigned int mask[] = {0,(unsigned)-1}; static unsigned int constant[] = {0, 1}; static inline int LLR_3(int a, int b) { int sign,c, numbers[2]; sign = (_rotl(a^b,1) & 1); c = ((_rotl(a,1) & 1)); a = (a^mask[c])+constant[c]; c = ((_rotl(b,1) & 1)); b = (b^mask[c])+constant[c]; numbers[0] = b; numbers[1] = a; c = ((_rotl(ab,1) & 1)); return (numbers[c]^mask[sign])+constant[sign]; }
      
      







LLR()_ 2の呼び出しをLLR_3()に置き換えると、大幅な増加は見られません(実行時間はほぼ同じ9.4秒ですが、3番目の文字が小さい方向にわずかに異なります)。 imulは最新のプロセッサではかなり高速であることがわかりました



すべてを開始したアルゴリズムに戻りましょう。 説明した1つの関数のみの最適化により、単一のデータブロックの処理時間が160秒から140秒に短縮されました(i7で実行した場合)。これは10%を超え、非常に良い結果です。 次に、同様の機能がいくつかあります...



最後に、最大2つの整数32ビット数を決定するための実装オプションを提案します。 それは純粋に学問的な好奇心から書かれました。 興味のある人は、急いでネタバレを見て、自分でこれを実装しようとすることはできません。 条件付きジャンプのないオプションは、標準の__max()マクロ(i7で実行した場合よりも約3倍高速に動作します。 プログラムを最適化してください。

非表示のテキスト
 static inline int max(int a, int b) { int numbers[2][2]; numbers[0][0] = b; numbers[0][1] = a; numbers[1][0] = a; numbers[1][1] = b; int sign_a = (unsigned)a>>31; int sign_b = (unsigned)b>>31; int sign_ab = (unsigned)(a^b)>>31; int abs_a = (1-2*sign_a)*a; int abs_b = (1-2*sign_b)*b; int sign_a_b = (unsigned)(abs_a-abs_b)>>31; int c0 = sign_ab; int c1 = ((1^(sign_a_b)^(sign_a | sign_b)) & (1^c0)) | (sign_ab & sign_a); int result = numbers[c0][c1]; return result; }
      
      









* UPD。 コメントでは、ユーザーshodanはrand()がタイムカウントから削除され、データが事前に形成された配列から読み取られた例を示しました。 この場合、パフォーマンスの向上は約3倍です。 どうやら、これは、アレイからデータを読み取る際のコンベアのより効率的な動作によるものです。



All Articles