4

Haswell の FMA 命令を使用した次の命令シーケンスを考えてみましょう。

  __m256 r1 = _mm256_xor_ps (r1, r1);
  r1 = _mm256_fmadd_ps (rp1, m6, r1);
  r1 = _mm256_fmadd_ps (rp2, m7, r1);
  r1 = _mm256_fmadd_ps (rp3, m8, r1);

  __m256 r2 = _mm256_xor_ps (r2, r2);
  r2 = _mm256_fmadd_ps (rp1, m3, r2);
  r2 = _mm256_fmadd_ps (rp2, m4, r2);
  r2 = _mm256_fmadd_ps (rp3, m5, r2);

  __m256 r3 = _mm256_xor_ps (r3, r3);
  r3 = _mm256_fmadd_ps (rp1, m0, r3);
  r3 = _mm256_fmadd_ps (rp2, m1, r3);
  r3 = _mm256_fmadd_ps (rp3, m2, r3);

同じ計算は、次のように非 FMA 命令を使用して表現できます。

  __m256 i1 = _mm256_mul_ps (rp1, m6);
  __m256 i2 = _mm256_mul_ps (rp2, m7);
  __m256 i3 = _mm256_mul_ps (rp3, m8);
  __m256 r1 = _mm256_xor_ps (r1, r1);
  r1 = _mm256_add_ps (i1, i2);
  r1 = _mm256_add_ps (r1, i3);

  i1 = _mm256_mul_ps (rp1, m3);
  i2 = _mm256_mul_ps (rp2, m4);
  i3 = _mm256_mul_ps (rp3, m5);
  __m256 r2 = _mm256_xor_ps (r2, r2);
  r2 = _mm256_add_ps (i1, i2);
  r2 = _mm256_add_ps (r2, i3);

  i1 = _mm256_mul_ps (rp1, m0);
  i2 = _mm256_mul_ps (rp2, m1);
  i3 = _mm256_mul_ps (rp3, m2);
  __m256 r3 = _mm256_xor_ps (r3, r3);
  r3 = _mm256_add_ps (i1, i2);
  r3 = _mm256_add_ps (r3, i3);

FMA バージョンでは、非 FMA バージョンよりもパフォーマンスが向上すると予想されます。

残念ながら、この場合、パフォーマンスの向上はゼロ (0) です。

誰かが理由を理解するのを手伝ってくれますか?

コア i7-4790 ベースのマシンで両方のアプローチを測定しました。

アップデート:

そこで、生成されたマシン コードを分析したところ、Haswell には 2 つの FMA パイプがあるため、r1 と r2 の依存関係チェーンが並行してディスパッチできるように、MSFT VS2013 C++ コンパイラがマシン コードを生成していると判断しました。

r3 は r1 の後にディスパッチする必要があるため、この場合、2 番目の FMA パイプはアイドル状態になります。

ループを展開して 3 セットではなく 6 セットの FMA を実行すれば、反復ごとにすべての FMA パイプをビジー状態に保つことができると考えました。

残念ながら、この場合のアセンブリ ダンプを確認したところ、MSFT コンパイラは、探していた種類の並列ディスパッチを許可するレジスタ割り当てを選択していませんでした。また、求めていたパフォーマンスの向上が得られなかったことが確認されました。為に。

C コードを (組み込み関数を使用して) 変更して、コンパイラがより良いコードを生成できるようにする方法はありますか?

4

2 に答える 2

6

周囲のループ (おそらく周囲のループがある)を含む完全なコード サンプルを提供していないため、明確に回答することは困難ですが、私が見た主な問題は、FMA コードの依存関係チェーンのレイテンシが乗算 + 加算コードよりもかなり長い。

FMA コードの 3 つのブロックはそれぞれ、同じ独立した操作を実行しています。

TOTAL += A1 * B1;
TOTAL += A2 * B2;
TOTAL += A3 * B3;

構造化されているため、各操作は合計で読み取りと書き込みを行うため、以前の期限に依存します。したがって、この一連の操作のレイテンシは、3 ops x 5 サイクル/FMA = 15 サイクルです。

FMA を使用せずに書き直したバージョンでは、次のことを行ったため、依存チェーンTOTALが壊れています。

TOTAL_1 = A1 * B1;  # 1
TOTAL_2 = A2 * B2;  # 2
TOTAL_3 = A3 * B3;  # 3

TOTAL_1_2 = TOTAL_1 + TOTAL2;  # 5, depends on 1,2
TOTAL = TOTAL_1_2 + TOTAL3;    # 6, depends on 3,5

最初の 3 つの MUL 命令は、依存関係がないため、独立して実行できます。2 つの加算命令は、乗算に順次依存します。したがって、このシーケンスのレイテンシは 5 + 3 + 3 = 11 です。

したがって、2 番目の方法の方がより多くの CPU リソースを使用しますが (合計 5 つの命令が発行されます)、レイテンシは低くなります。ループ全体がどのように構成されているかによっては、レイテンシーが低いと、このコードの FMA のスループットの利点が相殺される可能性があります (少なくとも部分的にレイテンシーが制限されている場合)。

より包括的な静的分析については、Intel の IACAを強くお勧めします。これは、上記のようなループ反復を実行し、少なくとも最良のシナリオでは、ボトルネックが何であるかを正確に教えてくれます。ループ内のクリティカル パス、遅延の制約があるかどうかなどを特定できます。

もう 1 つの可能性は、メモリ バウンド (レイテンシまたはスループット) であるということです。この場合、FMA と MUL + ADD で同様の動作が見られます。

于 2016-02-26T02:20:34.877 に答える
1

re: あなたの編集: コードには 3 つの依存関係チェーン (r1、r2、および r3) があるため、一度に 3 つの FMA を実行できます。Haswell の FMA は 5c のレイテンシであり、0.5c のスループットごとに 1 つなので、マシンは飛行中に 10 個の FMA を維持できます。

コードがループ内にあり、1 つの反復への入力が前の反復によって生成されていない場合、そのように 10 個の FMA を実行中に取得できます。(つまり、FMA を含むループ運搬依存関係チェーンはありません)。しかし、パフォーマンスの向上が見られないため、遅延によってスループットが制限される dep チェーンが存在する可能性があります。


MSVC から取得している ASM を投稿していませんが、レジスタの割り当てについて何かを主張しています。 xorps same,sameレジスタを書き込み専用オペランドとして使用するのと同じように (たとえば、非 FMA AVX 命令の宛先)、新しい依存関係チェーンを開始する認識されたゼロ化イディオムです。

コードが正しくても、r1 に対する r3 の依存関係が含まれている可能性はほとんどありません。レジスターの名前変更によるアウトオブオーダー実行により、別々の依存関係チェーンが同じレジスターを使用できることを理解していることを確認してください。


ところで、代わりに __m256 r1 = _mm256_xor_ps (r1, r1);を使用する必要があります__m256 r1 = _mm256_setzero_ps();。独自の初期化子で宣言している変数を使用しないでください。初期化されていないベクトルを使用すると、コンパイラはばかげたコードを作成することがあります。たとえば、スタック メモリからガベージをロードしたり、余分なxorps.

さらに良いのは次のとおりです。

__m256 r1 = _mm256_mul_ps (rp1, m6);
r1 = _mm256_fmadd_ps (rp2, m7, r1);
r1 = _mm256_fmadd_ps (rp3, m8, r1);

xorpsこれにより、アキュムレータのレジスタをゼロにする必要がなくなります。

Broadwell では、mulpsFMA よりもレイテンシが低くなります。

Skylake では、FMA/mul/add はすべて 4c レイテンシであり、0.5c スループットごとに 1 つです。彼らはポート1から別の加算器を落とし、FMAユニットでそれを行います. 彼らは、FMA ユニットのレイテンシーのサイクルを削減しました。

于 2016-03-26T01:27:08.983 に答える