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()関数を使用し、「スマート」コンパイラーに「最適化」させることでした。 24ns/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で改善されました。また、 Carmackの奇抜なNewton-Raphson近似手法 を試してみました。これは、ハードウェアよりも4.3ns/floatで実行しましたが、エラーは2分の1でした10 (これは私の目的には多すぎます)。
私がSSE opのreciprocal平方根を試した後、乗算を使用して平方根(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つの数学演算から合成するよりも遅いのはなぜですか?
私はこれを検証したので、これは実際には操作自体のコストであると確信しています:
(edit:stephentyroneは、数字の長い文字列に対する操作では、rsqrtps
のようなSIMDパックされたopのベクトル化を使用する必要があることを正しく指摘しています。ここでの配列データ構造は、テストのみを目的としています。私が実際に測定しようとしているのは、ベクトル化できないコードで使用するためのscalarパフォーマンスです。
sqrtss
は、正しく丸められた結果を返します。 rsqrtss
は、逆数に近似を与え、約11ビットまで正確です。
sqrtss
は、精度が必要な場合に、はるかに正確な結果を生成します。 rsqrtss
は、近似で十分な場合に存在しますが、速度が必要です。 Intelのドキュメントを読むと、ほぼ完全な精度(適切に覚えていれば〜23ビットの精度)を与える命令シーケンス(逆平方根近似とそれに続く単一のニュートン-ラプソンステップ)も見つかりますが、それでもある程度ですsqrtss
よりも高速です。
編集:速度が重要であり、実際に多くの値のループでこれを呼び出す場合は、これらの命令のベクトル化バージョンrsqrtps
またはsqrtps
、どちらも命令ごとに4つのフロートを処理します。
これは除算にも当てはまります。 MULSS(a、RCPSS(b))は、DIVSS(a、b)よりもずっと高速です。実際、Newton-Raphsonの反復で精度を上げても、なお高速です。
IntelおよびAMDは、最適化マニュアルでこの手法を推奨しています。 IEEE-754準拠を必要としないアプリケーションでは、div/sqrtを使用する唯一の理由はコードの可読性です。
答えを提供する代わりに、実際には間違っている可能性があります(キャッシュやその他のものについても確認したり、議論したりすることはありません。それらが同一であるとしましょう)。
違いは、sqrtとrsqrtの計算方法にある可能性があります。詳しくはこちらをご覧ください http://www.intel.com/products/processor/manuals/ 。私はあなたが使用しているプロセッサ関数について読むことから始めることをお勧めします、特にrsqrtに関するいくつかの情報があります(cpuは結果を得るのがはるかに簡単になる非常に近似した内部ルックアップテーブルを使用しています)。 rsqrtはsqrtよりもはるかに高速であるため、1つの追加のmul操作(これはコストがかかりません)がこの状況を変更しない可能性があります。
編集:言及する価値があるかもしれないいくつかの事実:
1。グラフィックライブラリの微最適化をいくつか行い、ベクトルの長さの計算にrsqrtを使用しました。 (sqrtの代わりに、2乗和にrsqrtを掛けました。これは、テストで行ったとおりです)、パフォーマンスが向上しました。
2。単純なルックアップテーブルを使用したrsqrtの計算は、xが無限大になると1/sqrt(x)が0になるとrsqrtのように簡単になる可能性があります。それは無限に行くので、それはその単純なケースです;)。
また、明確化:リンクした本のどこで見つけたのかわかりませんが、rsqrtがルックアップテーブルを使用していることを読んだことは確かです。正確である必要はありませんが、-少し前のように、私も間違っているかもしれません。
Newton-Raphsonは_-f/f'
_に等しい増分を使用してf(x)
のゼロに収束します。ここで_f'
_は導関数です。
x=sqrt(y)
の場合、f(x) = 0
;を使用してx
の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
_これも簡単です。