John Carmackには、Quake IIIソースコードに、フロートの逆平方根を計算する特別な関数があり、通常の(float)(1.0/sqrt(x))
、奇妙な0x5f3759df
定数。以下のコードを参照してください。ここで正確に何が起こっているのか、なぜこれが通常の実装よりもはるかに速く機能するのかを誰かが行ごとに説明できますか?
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y;
i = 0x5f3759df - ( i >> 1 );
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) );
#ifndef Q3_VM
#ifdef __linux__
assert( !isnan(y) );
#endif
#endif
return y;
}
参考までに。カーマックは書きませんでした。 Terje MathisenとGary Tarolliは、どちらも部分的な(そして非常に控えめな)クレジットを受け取り、他のソースもクレジットしています。
神話上の定数がどのように導出されたかは謎のようなものです。
ゲイリータロリを引用するには:
これは実際に整数で浮動小数点計算を行っています-これがどのようにそしてなぜ動作するのかを理解するのに長い時間がかかり、私はもう詳細を思い出すことができません。
わずかに優れた定数 専門の数学者によって開発された (Chris Lomont)は、元のアルゴリズムがどのように機能するかを試してみました:
float InvSqrt(float x)
{
float xhalf = 0.5f * x;
int i = *(int*)&x; // get bits for floating value
i = 0x5f375a86 - (i >> 1); // gives initial guess y0
x = *(float*)&i; // convert bits back to float
x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
return x;
}
これにもかかわらず、彼のidのsqrtの数学的に「優れた」バージョン(ほぼ同じ定数になった)の試みは、数学的にはるかに「純粋」であるにもかかわらずGaryによって最初に開発されたものよりも劣っていることを証明しました。彼は、なぜidがとても優れたiircだったのか説明できませんでした。
もちろん、最近では、FPUのsqrt(特に360/PS3)を使用するよりもはるかに遅いことがわかりました。ハードウェアのルート。
基礎となるハードウェアの性質が変化するにつれて、最適化がどのように進化する必要があるかを示しています。
Greg HewgillおよびIllidanS4は、数学的説明に優れたリンクを提供しました。ここでは、あまり詳しく説明したくない人のためにまとめてみます。
いくつかの例外を除いて、任意の数学関数は多項式の和で表すことができます。
_y = f(x)
_
exactlyに変換できます:
_y = a0 + a1*x + a2*(x^2) + a3*(x^3) + a4*(x^4) + ...
_
ここで、a0、a1、a2、...は定数です。問題は、平方根のような多くの関数では、正確な値に対して、この合計のメンバー数は無限であり、x ^ nで終わらないことです。しかし、いくつかのx ^ nで停止すると、ある程度の精度まで結果が得られます。
だから、もし持っているなら:
_y = 1/sqrt(x)
_
この特定のケースでは、おそらく計算速度のために、2番目を超えるすべての多項式メンバーを破棄することにしました。
_y = a0 + a1*x + [...discarded...]
_
そして、yが正確な値との差を最小にするために、タスクはa0とa1を計算するようになりました。最も適切な値は次のように計算されています。
_a0 = 0x5f375a86
a1 = -0.5
_
したがって、これを方程式に入れると、次のようになります。
_y = 0x5f375a86 - 0.5*x
_
これは、コードに表示される行と同じです。
_i = 0x5f375a86 - (i >> 1);
_
編集:実際にはここで_y = 0x5f375a86 - 0.5*x
_はi = 0x5f375a86 - (i >> 1);
と同じではありませんアーティファクトですが、それでもいくつかの係数a0、a1、a2 ...を計算することになります。
この時点で、彼らはこの結果の精度が目的には十分でないことを発見しました。そのため、結果の精度を向上させるために、Newtonの反復の1ステップのみを追加しました。
_x = x * (1.5f - xhalf * x * x)
_
必要な精度が満たされるまで、ループ内でさらにいくつかの反復を行い、それぞれが結果を改善することができました。 これはまさにそれがCPU/FPUで動作する方法です!しかし、1回の反復で十分であるようで、これも速度の祝福でした。 CPU/FPUは、結果が保存される浮動小数点数の精度に達するために必要なだけ反復を行い、すべての場合に機能するより一般的なアルゴリズムを備えています。
要するに、彼らがしたことは:
CPU/FPUと(ほぼ)同じアルゴリズムを使用し、1/sqrt(x)の特殊なケースの初期条件の改善を活用し、CPU/FPUは先に進みますが、より早く停止するため、計算速度が向上します。
この素敵な記事による しばらく前に書かれた...
コードの魔法は、たとえそれを追うことができなくても、i = 0x5f3759df-(i >> 1);として際立っています。ライン。簡略化されたNewton-Raphsonは、推測で始まり、反復で洗練される近似です。 32ビットx86プロセッサの性質を利用して、整数であるiは、整数キャストを使用して、最初に逆二乗する浮動小数点数の値に設定されます。次に、iを0x5f3759dfに設定し、マイナス自体を1ビット右にシフトします。右シフトはiの最下位ビットをドロップし、本質的にそれを半分にします。
とても良い読み物です。これはほんの一部です。
定数が浮動小数点数であるかどうかを知りたいので、このコードを書いて、飛び出した整数をグーグルで調べました。
long i = 0x5F3759DF;
float* fp = (float*)&i;
printf("(2^127)^(1/2) = %f\n", *fp);
//Output
//(2^127)^(1/2) = 13211836172961054720.000000
定数は「16進数形式の浮動小数点表現0x5f3759dfでよく知られている2 ^ 127の平方根の整数近似」 https://mrob.com/pub/math/numbers -18.html
同じサイトで、全体を説明しています。 https://mrob.com/pub/math/numbers-16.html#le009_16