1

私はアルゴリズムのボトルネックである数千時間と呼ばれるループ内の命令のブロックを最適化しようとしています。

このコード ブロックは、N ベクトル 3 (iV 配列) に対する N 行列 3x3 (iA 配列) の乗算を計算し、N の結果を oV 配列に格納します。(N は固定ではなく、通常は 3000 から 15000 の間です)

行列とベクトルの各行は、SSE 最適化を利用するために 128 ビット (4 つの float) に揃えられます (4 番目の浮動値は無視されます)。

C++ コード:

  __m128* ip = (__m128*)iV;
  __m128* op = (__m128*)oV;
  __m128* A = (__m128*)iA;

  __m128 res1, res2, res3;
  int i;

  for (i=0; i<N; i++)
  {
    res1 = _mm_dp_ps(*A++, *ip, 0x71);
    res2 = _mm_dp_ps(*A++, *ip, 0x72);
    res3 = _mm_dp_ps(*A++, *ip++, 0x74);

    *op++ = _mm_or_ps(res1, _mm_or_ps(res2, res3));
  }

コンパイラは次の命令を生成します。

000007FEE7DD4FE0  movaps      xmm2,xmmword ptr [rsi]               //move "ip" in register
000007FEE7DD4FE3  movaps      xmm1,xmmword ptr [rdi+10h]           //move second line of A in register
000007FEE7DD4FE7  movaps      xmm0,xmmword ptr [rdi+20h]           //move third line of A in register
000007FEE7DD4FEB  inc         r11d                                 //i++
000007FEE7DD4FEE  add         rbp,10h                              //op++
000007FEE7DD4FF2  add         rsi,10h                              //ip++
000007FEE7DD4FF6  dpps        xmm0,xmm2,74h                        //dot product of 3rd line of A against ip
000007FEE7DD4FFC  dpps        xmm1,xmm2,72h                        //dot product of 2nd line of A against ip
000007FEE7DD5002  orps        xmm0,xmm1                            //"merge" of the result of the two dot products
000007FEE7DD5005  movaps      xmm3,xmmword ptr [rdi]               //move first line of A in register
000007FEE7DD5008  add         rdi,30h                              //A+=3
000007FEE7DD500C  dpps        xmm3,xmm2,71h                        //dot product of 1st line of A against ip
000007FEE7DD5012  orps        xmm0,xmm3                            //"merge" of the result
000007FEE7DD5015  movaps      xmmword ptr [rbp-10h],xmm0           //move result in memory (op)
000007FEE7DD5019  cmp         r11d,dword ptr [rbx+28h]             //compare i
000007FEE7DD501D  jl          MyFunction+370h (7FEE7DD4FE0h)       //loop

私は低レベルの最適化にあまり詳しくないので、質問は次のとおりです。アセンブリ コードを自分で書いた場合、可能な最適化がいくつか見られますか?

たとえば、次のように変更すると、より速く実行されますか?

add         rbp,10h
movaps      xmmword ptr [rbp-10h],xmm0

movaps      xmmword ptr [rbp],xmm0
add         rbp,10h

また、ADD命令はINCよりも高速であることも読みました...

4

3 に答える 3

3

などのオフセットを使用した間接アドレスの計算rbp-10は非常に安価です。これは、「実効アドレス計算」ユニットにこれらの種類の計算用の特別なハードウェアがあるためです [これは別の名前だと思いますが、考えられないか、成功することはありませんグーグルでその名前を見つけてください]。

ただし、 と の間には依存関係がadd rbp,10hあり[rbp-10h]、問題が発生する可能性がありますが、この特定のケースでは疑わしいと思います。あなたの場合、それを使用するまでの距離が長いrbp-10ので、問題ありません。コンパイラーは、その時点で「フリー」であるため、おそらくそれをそこまで上げています。これは、プロセッサーが、以前に読み取られた xmm レジスターに外部からデータが入るのを待っているためです。言い換えれば、ループの開始時にxmm0xmm1の読み取りとを使用する命令の間に固執できるすべての作業は有益です。なぜなら、プロセッサはそのデータが「到着」するのを待ってから計算できるからです。結果。xmm2dppsxmm0xmm1xmm2dpps

于 2013-08-09T14:45:11.670 に答える
2

私は多くの x86 アセンブリの最適化を行ってきましたが、それは素晴らしい学習体験だったと言えます。コンパイラーがどのように機能するかについても多くのことを学びました。私が学んだ最大のことは、コンパイラーは一般的に、その機能がかなり優れているということでした。私はそれが軽薄なコメントであることを知っていますが、それは本当です...

また、行った最適化は、あるプロセッサ ファミリではプラスの結果をもたらし、別のプロセッサ ファミリではマイナスの結果をもたらす可能性があることも学びました。パイプライン処理、分岐予測、プロセッサ キャッシュなどは大きな役割を果たします。そのため、非常に特定のハードウェア構成をターゲットにしている場合を除き、改善に関する仮定には注意してください。

追加を並べ替えてオフセットを削除することについての特定の質問に対してrbp-10h...明らかな改善のように見え、取扱説明書を読んで確認する必要がありますが、-10hメモリオフセットはその命令で無料で提供されると思います。また、 を移動するaddと、パイプライン化された命令がスローされ、実際にはクロック サイクルの損失が発生する可能性があります。実験する必要があります。

于 2013-08-09T14:29:13.667 に答える
1

上記のコードを改善するためにできることがいくつかあります。一般に、値を変更した後にその値を使用すると、結果を待つ間にプロセッサが停止します。したがって、これらの行にはペナルティが発生します:-

add         rbp,10h
movaps      xmmword ptr [rbp-10h],xmm0

しかし、これら 2 行の上のコード スニペットではかなり離れているため、実際には問題になりません。他の人がすでに言ったrbp-10hように、アドレス計算ハードウェアがそれを処理するという点で「無料」です。

行を上に移動しmovaps xmm3,xmmword ptr [rdi]て、他の行をいくつか再配置することができます。

それは価値があるでしょうか?

いいえ

あなたのアルゴリズムは

<blink> memory bandwidth limited </blink>*

つまり、RAM から CPU にデータを読み込むのにかかる時間は、CPU が処理を行うのにかかる時間よりも長くなります。最悪の場合、メモリ アドレスの読み取りに、ページ フォールトとディスク読み取りが含まれる可能性があります。prefetch命令も役に立ちません。これは、データを CPU にストリーミングするように最適化されているため、「ストリーミング SIMD 拡張」と呼ばれます (メモリ インターフェイスは 4 つの個別のストリーム IIRC を処理できます) 。

小さなデータ セット (おそらく FFT) に対して多くの計算を行っていた場合、アセンブラを手作りすることで多くのメリットが得られます。しかし、あなたのアルゴリズムは非常に単純であるため、CPU はデータの到着を待機するために多くの時間をアイドル状態にしています。RAM の速度がわかっている場合は、アルゴリズムの最大スループットを計算し、それを使用して現在達成されているものと比較できます (ただし、理論上の最大スループットに到達することはありません)。

メモリのストールを最小限に抑えるためにできることはありますが、それは個々の命令をいじるのではなく、より高いレベルの変更です (多くの場合、アルゴリズムを最適化するとより良い結果が得られます)。最も簡単な方法は、入力データをダブル バッファリングすることです。レジスタ セットを 2 つのグループに分割します (SIMD レジスタを 4 つしか使用していないため、ここで行うことができます)。

  load set 1
mainloop:
  load set 2
  do processing on set 1
  save set 1 result
  load set 1
  do processing on set 2
  save set 2 result
  goto mainloop

うまくいけば、それはあなたにいくつかのアイデアを与えました. それほど速くはなりませんが、それでも興味深いエクササイズであり、そこから多くのことを学ぶことができます.

  • RIP点滅。
于 2013-08-09T15:21:45.853 に答える