SSEスカラーsqrt(x)がrsqrt(x)* xより遅いのはなぜですか?


106

私はIntel Core Duoでいくつかのコア計算をプロファイリングしており、平方根へのさまざまなアプローチを検討しているときに、奇妙なことに気付きました:SSEスカラー演算を使用すると、逆平方根を取得して乗算する方が高速ですネイティブのsqrtオペコードを使用するよりも、sqrtを取得する方が便利です。

私はそれを次のようなループでテストしています:

inline float TestSqrtFunction( float in );

void TestFunc()
{
  #define ARRAYSIZE 4096
  #define NUMITERS 16386
  float flIn[ ARRAYSIZE ]; // filled with random numbers ( 0 .. 2^22 )
  float flOut [ ARRAYSIZE ]; // filled with 0 to force fetch into L1 cache

  cyclecounter.Start();
  for ( int i = 0 ; i < NUMITERS ; ++i )
    for ( int j = 0 ; j < ARRAYSIZE ; ++j )
    {
       flOut[j] = TestSqrtFunction( flIn[j] );
       // unrolling this loop makes no difference -- I tested it.
    }
  cyclecounter.Stop();
  printf( "%d loops over %d floats took %.3f milliseconds",
          NUMITERS, ARRAYSIZE, cyclecounter.Milliseconds() );
}

TestSqrtFunctionのいくつかの異なるボディでこれを試してみましたが、本当に頭を悩ましているいくつかのタイミングがあります。最悪なのは、ネイティブのsqrt()関数を使用して、「スマート」コンパイラーを「最適化」することです。24 ns / floatで、x87 FPUを使用すると、これは明らかに悪いことでした。

inline float TestSqrtFunction( float in )
{  return sqrt(in); }

次に試したのは、コンパイラにSSEのスカラーsqrtオペコードを使用させるために組み込み関数を使用することでした。

inline void SSESqrt( float * restrict pOut, float * restrict pIn )
{
   _mm_store_ss( pOut, _mm_sqrt_ss( _mm_load_ss( pIn ) ) );
   // compiles to movss, sqrtss, movss
}

これは11.9ns / floatでより優れていました。また、カーマックの奇抜なニュートンラフソン近似手法も試してみました。これは、ハードウェアよりも4.3ns / floatで実行されましたが、エラーは1 in 2 10でした(これは、私の目的には多すぎます)。

危険なのは、平方根のSSE演算を試し、乗算を使用して平方根を取得したときです(x * 1 /√x=√x)。これには2つの依存する演算が必要ですが、1.24ns / floatで2 -14までの精度で、これははるかに高速なソリューションでした。

inline void SSESqrt_Recip_Times_X( float * restrict pOut, float * restrict pIn )
{
   __m128 in = _mm_load_ss( pIn );
   _mm_store_ss( pOut, _mm_mul_ss( in, _mm_rsqrt_ss( in ) ) );
   // compiles to movss, movaps, rsqrtss, mulss, movss
}

私の質問は基本的に何が与えるのですか?SSEの組み込みからハードウェアへの平方根オペコードが他の2つの数学演算から合成するより遅いのはなぜですか?

私は確認したので、これは実際にはオペレーション自体のコストであると確信しています:

  • すべてのデータはキャッシュに収まり、アクセスはシーケンシャルです
  • 関数はインライン化されます
  • ループを展開しても違いはありません
  • コンパイラー・フラグは完全最適化に設定されています(そして、アセンブリーは良好です。

編集:stephentyroneは、数値の長い文字列の操作では、ベクトル化SIMDパック演算を使用する必要があることを正しく指摘していますrsqrtpsが、ここでの配列データ構造は、テスト目的のみです。私が実際に測定しようとしているのは、コードで使用するスカラーパフォーマンスです。ベクトル化できません。)


13
x / sqrt(x)= sqrt(x)。または、別の言い方をすると:x ^ 1 * x ^(-1/2)= x ^(
1-1

6
もちろん、inline float SSESqrt( float restrict fIn ) { float fOut; _mm_store_ss( &fOut, _mm_sqrt_ss( _mm_load_ss( &fIn ) ) ); return fOut; }。しかし、これは悪い考えです。CPUがフロートをスタックに書き込み、すぐにそれらを読み戻すと、ロードヒットストアのストールを簡単に引き起こす可能性があるためです。特に、戻り値のベクトルレジスタからフロートレジスタにジャグリング悪い知らせです。さらに、SSEコンパイラ組み込み関数が表す基になるマシンオペコードは、とにかくアドレスオペランドを取ります。
Crashworks、2010

4
LHSの重要度は特定のx86の特定の世代とステッピングに依存します。私の経験では、i7までの何でも、レジスタセット間でデータを移動すること(例:FPUからSSEへeax)は非常に悪いですが、xmm0とスタックの間の往復Intelのストアフォワーディングのため、戻り値は異なります。あなたは確かに自分でそれを見ることができます。一般に、潜在的なLHSを確認する最も簡単な方法は、発行されたアセンブリを調べて、レジスタセット間でデータがやり取りされる場所を確認することです。コンパイラが賢いこともあれば、そうでないこともあります。正規化ベクトルに関しては、私はここに私の結果を書いた:bit.ly/9W5zoU
Crashworks

2
PowerPCについては、そうです。IBMには、静的分析を通じてLHSおよび他の多くのパイプラインバブルを予測できるCPUシミュレーターがあります。一部のPPCには、ポーリングできるLHSのハードウェアカウンターもあります。x86の方が難しいです。優れたプロファイリングツールは不足しており(最近のVTuneは多少壊れています)、並べ替えられたパイプラインの確定性は低くなっています。ハードウェアパフォーマンスカウンターを使用して正確に実行できる、サイクルごとの命令を測定することで、経験的に測定することができます。「リタイアされた命令」および「合計サイクル」レジスタは、たとえばPAPIまたはPerfSuite(bit.ly/an6cMt)で読み取ることができます。
Crashworks、2010

2
また、関数にいくつかの順列を記述して、特にストールに悩まされているかどうかを確認するために時間を調整することもできます。Intelは、パイプラインの動作方法に関する詳細をあまり公開していません(LHSはまったく秘密の秘密です)。したがって、私が学んだことの多くは、他のアーチ(PPCなど)でストールを引き起こすシナリオを見ることです。 )、そしてx86にもそれがあるかどうかを確認するために制御された実験を構築します。
Crashworks、2010

回答:


216

sqrtss正しく丸められた結果が得られます。 rsqrtss与え近似約11ビットの精度の逆数にし、。

sqrtss精度が必要な場合のために、はるかに正確な結果を生成しています。 rsqrtss近似で十分ですが、速度が必要な場合のために存在します。Intelのドキュメントを読むと、ほぼ完全な精度(私が正しく覚えていれば23ビットの精度)を提供する命令シーケンス(逆平方根近似とそれに続く単一のニュートンラフソンステップ)も見つかります。よりも速いsqrtss

編集:速度が重要であり、多くの値に対してループでこれを実際に呼び出している場合は、これらの命令のベクトル化バージョンを使用するrsqrtpssqrtps、どちらも1つの命令で4つの浮動小数点を処理する必要があります。


3
n / rステップでは、22ビットの精度が得られます(2倍になります)。23ビットは完全に正確です。
Jasper Bekkers、2011

7
@Jasper Bekkers:いいえ、そうではありません。まず、floatの精度は24ビットです。次に、sqrtss正しく丸められます。丸め前に約50ビットが必要であり、単精度での単純なN / R反復を使用して実現することはできません。
スティーブンキャノン

1
これは間違いなく理由です。この結果を拡張するには:IntelのEmbreeプロジェクト(software.intel.com/en-us/articles/…)は、数学にベクトル化を使用しています。そのリンクからソースをダウンロードして、彼らが3/4 Dベクトルをどのように実行するかを見ることができます。それらのベクトル正規化はrsqrtを使用し、その後にnewton-raphsonの反復が続きます。これは非常に正確で、1 / ssqrtよりも高速です!
Brandon Pelfrey

7
小さな警告:x が0または無限大の場合、 x rsqrt(x)はNaNになります。0 * rsqrt(0)= 0 * INF = NaN。INF rsqrt(INF)= INF * 0 = NaN。このため、NVIDIA GPU上のCUDAは、近似単精度平方根をrecip(rsqrt(x))として計算し、ハードウェアは逆数と逆数平方根の両方に高速近似を提供します。明らかに、2つの特殊なケースを処理する明示的なチェックも可能です(ただし、GPUでは遅くなります)。
njuffa

@BrandonPelfreyどのファイルでニュートンラプソンステップを見つけましたか?
fredoverflow 2013

7

これは除算にも当てはまります。MULSS(a、RCPSS(b))は、DIVSS(a、b)よりもはるかに高速です。実際、ニュートンラフソン反復法を使用して精度を上げても、さらに高速です。

IntelとAMDはどちらも、最適化マニュアルでこの手法を推奨しています。IEEE-754準拠を必要としないアプリケーションでは、div / sqrtを使用する唯一の理由はコードの読みやすさです。


1
Broadwellマイクロアーキテクチャと打ち鳴らすなどのコンパイラはそれが普通だから、最近のCPU上のスカラーのための往復+ニュートンを使用しないことを選択したので、後に、より良いFP除算性能を持っていない速いです。ほとんどのループでdivは、これが唯一の操作ではないため、divpsまたはが存在する場合でも、uopの合計スループットがボトルネックになることがよくありdivssます。浮動小数点除算と浮動小数点乗算を参照してください。私の答えには、なぜrcppsスループットが勝てないのかに関するセクションがあります。(またはレイテンシの勝利)、および分割スループット/レイテンシの数値。
Peter Cordes

精度要件が低すぎてニュートンの反復をスキップできる場合は、はいのa * rcpss(b)方が速くなりますが、それでもuopsは高くなりa/bます。
Peter Cordes

5

答えを提供する代わりに、それは実際には正しくない可能性があります(キャッシュやその他のものについてもチェックしたり議論したりはしませんが、それらが同一であるとしましょう)。
違いは、sqrtとrsqrtの計算方法にある可能性があります。詳細については、http://www.intel.com/products/processor/manuals/をご覧ください。私はあなたが使用しているプロセッサ関数について読むことから始めることをお勧めします、特にrsqrtに関するいくつかの情報があります(cpuは巨大な近似の内部ルックアップテーブルを使用しているため、結果を取得するのがはるかに簡単になります)。rsqrtはsqrtよりもはるかに高速であり、1つの追加のmul操作(コストは高くありません)で状況が変更されないように見えるかもしれません。

編集:言及する価値のあるいくつかの事実:
1.グラフィックライブラリに対していくつかのマイクロ最適化を行っていて、ベクトルの長さの計算にrsqrtを使用したことがある。(sqrtの代わりに、2乗の合計にそのrsqrtを掛けました。これは、テストで行ったとおりです)。
2. rsqrtのように、単純なルックアップテーブルを使用してrsqrtを計算する方が簡単かもしれません。 sqrt-それは無限に行くので、それはその単純なケースです;)。

また、明確化:リンクした本のどこにあるかはわかりませんが、rsqrtがいくつかのルックアップテーブルを使用していることを読んだことは確かです。正確である必要はありませんが、少し前のように、私も間違っている可能性があります。


4

ニュートンラプソンは、f(x)増分が-f/f' どこにあるかf'と等しい増分を使用してゼロに収束します。

についてはx=sqrt(y)f(x) = 0x使用して解決しようとすることができf(x) = x^2 - yます。

次に、増分は次のとおりです。dx = -f/f' = 1/2 (x - y/x) = 1/2 (x^2 - y) / x これには、ゆっくりとした分割があります。

他の関数(などf(x) = 1/y - 1/x^2)を試すこともできますが、それらも同様に複雑になります。

1/sqrt(y)今見てみましょう。を試すこともできますが、たとえば、f(x) = x^2 - 1/y同じように複雑dx = 2xy / (y*x^2 - 1)になります。の非自明な代替選択肢f(x)は次のとおりです。f(x) = y - 1/x^2

次に: dx = -f/f' = (y - 1/x^2) / (2/x^3) = 1/2 * x * (1 - y * x^2)

ああ!ささいな表現ではありませんが、乗算はあり、除算はありません。=>より速く!

そして、完全な更新ステップnew_x = x + dxは次のようになります:

x *= 3/2 - y/2 * x * x これも簡単です。


2

これに対する他の多くの回答が数年前からすでにあります。コンセンサスが正しいものは次のとおりです。

  • rsqrt *命令は、逆平方根の近似値を計算します。約11〜12ビットで十分です。
  • これは、仮数によってインデックスが付けられたルックアップテーブル(つまりROM)を使用して実装されます。(実際、これは古い数学テーブルに似た圧縮ルックアップテーブルであり、下位ビットの調整を使用してトランジスタを節約しています。)
  • これが使用できる理由は、「実際の」平方根アルゴリズムのFPUで使用される初期推定値だからです。
  • おおよその逆数の命令、rcpもあります。これらの命令はどちらも、FPUが平方根と除算を実装する方法の手掛かりです。

コンセンサスが間違っているのはここにあります:

  • SSE時代のFPUは平方根の計算にNewton-Raphsonを使用しません。これはソフトウェアでは優れた方法ですが、ハードウェアでそのように実装するのは誤りです。

他の人が指摘しているように、逆平方根を計算するNRアルゴリズムにはこの更新ステップがあります。

x' = 0.5 * x * (3 - n*x*x);

これは、データに依存する多くの乗算と1つの減算です。

以下は、最新のFPUが実際に使用するアルゴリズムです。

が与えられた場合b[0] = n、1に近づくY[i]ような一連の数値を見つけることができると仮定しb[n] = b[0] * Y[0]^2 * Y[1]^2 * ... * Y[n]^2ます。次に、以下を考慮します。

x[n] = b[0] * Y[0] * Y[1] * ... * Y[n]
y[n] = Y[0] * Y[1] * ... * Y[n]

明らかにx[n]近づきsqrt(n)y[n]アプローチ1/sqrt(n)

Newton-Raphson更新ステップを使用して、逆平方根を取得し、良い結果を得ることができY[i]ます。

b[i] = b[i-1] * Y[i-1]^2
Y[i] = 0.5 * (3 - b[i])

次に:

x[0] = n Y[0]
x[i] = x[i-1] * Y[i]

そして:

y[0] = Y[0]
y[i] = y[i-1] * Y[i]

次の重要な観察はそれb[i] = x[i-1] * y[i-1]です。そう:

Y[i] = 0.5 * (3 - x[i-1] * y[i-1])
     = 1 + 0.5 * (1 - x[i-1] * y[i-1])

次に:

x[i] = x[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
     = x[i-1] + x[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))
y[i] = y[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
     = y[i-1] + y[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))

つまり、初期のxとyを指定すると、次の更新ステップを使用できます。

r = 0.5 * (1 - x * y)
x' = x + x * r
y' = y + y * r

または、さらに洗練された設定も可能ですh = 0.5 * y。これは初期化です:

Y = approx_rsqrt(n)
x = Y * n
h = Y * 0.5

そして、これは更新手順です:

r = 0.5 - x * h
x' = x + x * r
h' = h + h * r

これはGoldschmidtのアルゴリズムであり、ハードウェアで実装する場合に非常に有利です。「内部ループ」は3つの積和演算で他には何もありません。2つは独立しており、パイプライン化できます。

1999年には、FPUはすでにパイプライン化された加算/減算回路とパイプライン化された乗算回路を必要としていましたが、そうでなければSSEはあまり「ストリーミング」されませんでした。平方根だけで多くのハードウェアを無駄にすることなく、完全にパイプライン化された方法でこの内部ループを実装するために、1999年には各回路の1つだけが必要でした。

もちろん、今日、プログラマーに公開された乗加算を融合しました。繰り返しになりますが、内側のループは3つのパイプラインFMAであり、平方根を計算していない場合でも(再び)一般に役立ちます。


1
関連:GCCのsqrt()はコンパイル後にどのように機能しますか?ルートのどの方法が使用されていますか?ニュートンラプソン?ハードウェアdiv / sqrt実行ユニットの設計へのリンクがいくつかあります。 高速ベクトル化されたrsqrtと、精度に応じてSSE / AVXとの逆数-Haswell_mm256_rsqrt_psパフォーマンス分析で、FMAの有無にかかわらず、ソフトウェアで1ニュートン反復。通常、ループ内に他の作業がなく、分周器のスループットでボトルネックになる可能性がある場合にのみ良いアイデアです。HW sqrtは単一のuopなので、他の作業と混合しても問題ありません。
Peter Cordes

-2

これらの命令は丸めモードを無視し、浮動小数点例外や非正規化数を処理しないため、処理が速くなります。これらの理由により、他のfp命令を順不同でパイプライン処理、推測、および実行する方がはるかに簡単です。


明らかに間違っています。FMAは現在の丸めモードに依存しますが、Haswell以降では1クロックあたり2のスループットがあります。2つの完全にパイプライン化されたFMAユニットにより、Haswellは一度に最大10個のFMAを飛行させることができます。正しい答えはrsqrtの 精度がはるかに低いことです。つまり、テーブルルックアップを行って最初の推測を行うと、実行する作業がはるかに少なくなります(または何も実行されませんか?)。
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.