現在、さまざまな最新のプロセッサの高速単精度浮動小数点相互補完機能を使用して、64ビットの開始近似を計算する方法を検討しています固定小数点Newton-Raphson反復に基づく符号なし整数除算。可能な限り正確に2/divisorの計算が必要で、次の固定小数点反復の要件に基づいて、初期近似が数学的結果より小さくなければならない。これは、この計算が過小評価を提供する必要があることを意味します。私は現在、広範囲なテストに基づいて、うまく機能次のコードを、持っている:高速浮動小数点の逆数による2 ** 64 /除数の効率的な計算
#include <stdint.h> // import uint64_t
#include <math.h> // import nextafterf()
uint64_t divisor, recip;
float r, s, t;
t = uint64_to_float_ru (divisor); // ensure t >= divisor
r = 1.0f/t;
s = 0x1.0p64f * nextafterf (r, 0.0f);
recip = (uint64_t)s; // underestimate of 2**64/divisor
このコードが機能しているが、それはほとんどのプラットフォーム上で正確に速くありません。機械固有のコードを少し必要とする明らかな改善の1つは、ハードウェアによって提供される高速浮動小数点の逆数を利用するコードで除算r = 1.0f/t
を置き換えることです。これは、数学的結果の1 ulp以内の結果を生成するために反復処理で拡張することができるため、過小評価は既存のコードのコンテキストで生成されます。 x86_64のためのサンプル実装は次のようになります。nextafterf()
の
#include <xmmintrin.h>
/* Compute 1.0f/a almost correctly rounded. Halley iteration with cubic convergence */
inline float fast_recip_f32 (float a)
{
__m128 t;
float e, r;
t = _mm_set_ss (a);
t = _mm_rcp_ss (t);
_mm_store_ss (&r, t);
e = fmaf (r, -a, 1.0f);
e = fmaf (e, e, e);
r = fmaf (e, r, r);
return r;
}
実装は、一般的にパフォーマンスが最適化されていません。
s = int_as_float (float_as_int (r) + 0x1fffffff);
をこれらのアプローチであると仮定すると、次のように組み込み関数float_as_int()
とint_as_float()
を通じて迅速IEEE int32
およびその逆に754 binary32
を再interpreteするための手段があるプラットフォームでは、我々はnextafterf()
とスケーリングの使用を組み合わせることができます可能であれば、float
とuint64_t
の間の変換が大きな障害となります。ほとんどのプラットフォームは、uint64_t
からfloat
へのスタティックな丸めモード(ここでは正の無限大=上向き)への変換を実行する命令を提供しません。また、uint64_t
と浮動小数点型の間の変換を指示しないものもあります。パフォーマンスのボトルネック。
t = uint64_to_float_ru (divisor);
r = fast_recip_f32 (t);
s = int_as_float (float_as_int (r) + 0x1fffffff);
recip = (uint64_t)s; /* underestimate of 2**64/divisor */
がuint64_to_float_ru
のポータブルが、遅い、実装はFPU丸めモードを動的に変更使用しています:私は変換に対処するための様々な分割とビットいじるのアプローチに見てきました
#include <fenv.h>
#pragma STDC FENV_ACCESS ON
float uint64_to_float_ru (uint64_t a)
{
float res;
int curr_mode = fegetround();
fesetround (FE_UPWARD);
res = (float)a;
fesetround (curr_mode);
return res;
}
を(行う例整数側を四捨五入してからfloat
への通常の変換を使用します。これは、IEEE 754丸めモードを使用して最近接または偶数に丸めますが、これによってオーバーヘッドが発生するため、この計算は高速浮動小数点の逆数視点。それは、補間を伴う古典的なLUT、または固定小数点多項式近似を使用して開始近似を生成し、32ビットの固定小数点ニュートン・ラフソン・ステップでそれらをフォローする方が良いようです。
私の現在のアプローチの効率を改善する方法はありますか?特定のプラットフォーム用の組み込み関数を含むポータブルおよび準ポータブルな方法が関心があります(特に、現在支配的なCPUアーキテクチャであるx86およびARMの場合)。非常に高い最適化(/O3 /QxCORE-AVX2 /Qprec-div-
)でIntelコンパイラを使用してx86_64をコンパイルすると、初期近似の計算には反復より多くの命令が必要になります。これには約20命令が必要です。以下は、参考のための完全な除算コードであり、近似をコンテキストで示しています。
uint64_t udiv64 (uint64_t dividend, uint64_t divisor)
{
uint64_t temp, quot, rem, recip, neg_divisor = 0ULL - divisor;
float r, s, t;
/* compute initial approximation for reciprocal; must be underestimate! */
t = uint64_to_float_ru (divisor);
r = 1.0f/t;
s = 0x1.0p64f * nextafterf (r, 0.0f);
recip = (uint64_t)s; /* underestimate of 2**64/divisor */
/* perform Halley iteration with cubic convergence to refine reciprocal */
temp = neg_divisor * recip;
temp = umul64hi (temp, temp) + temp;
recip = umul64hi (recip, temp) + recip;
/* compute preliminary quotient and remainder */
quot = umul64hi (dividend, recip);
rem = dividend - divisor * quot;
/* adjust quotient if too small; quotient off by 2 at most */
if (rem >= divisor) quot += ((rem - divisor) >= divisor) ? 2 : 1;
/* handle division by zero */
if (divisor == 0ULL) quot = ~0ULL;
return quot;
}
umul64hi()
は一般に固有のプラットフォーム固有の、またはインラインアセンブリコードのビットにマッピングすることになります。 x86_64版では私は現在、この実装を使用します。
inline uint64_t umul64hi (uint64_t a, uint64_t b)
{
uint64_t res;
__asm__ (
"movq %1, %%rax;\n\t" // rax = a
"mulq %2;\n\t" // rdx:rax = a * b
"movq %%rdx, %0;\n\t" // res = (a * b)<63:32>
: "=rm" (res)
: "rm"(a), "rm"(b)
: "%rax", "%rdx");
return res;
}
が浮いていることを考えると...
は、私はあなたが欲しい精度を得るために複数の1または2ニュートン・ラプソン反復が必要になります(精度の唯一の23ビットで)フロートを疑うが、私は数学を行っていませんあなたのISAがそれをサポートしていると仮定し、コンパイラにそうしたと仮定すれば、最適化されたコードを発行するのにコンパイラがスマートであってはいけませんか? –
@JohnZwinck多分:-)通常、コンパイラスイッチを操作することで、望ましくない方法で他のコードに悪影響を及ぼします。組み込み関数はうまくいきますが、しばしば、プラットフォーム固有のものに密接にマップされる一連の「汎用組み込み関数」に抽象化することができます(GROMACSのSIMDソースコードを参考にしてください)。いずれにしても、浮動小数点の相反は実際には私の問題ではなく、GPUを除いて、私のアプローチを壊しています。 – njuffa
ベンチマークしましたか?どうやって?どのターゲット詳細?どのツールチェーン?結果は何でしたか?なぜあなたのコードで「コンパイラスイッチを操作する」必要がないと思いますか?生成されたコードを完全に制御したい場合、最終的にはアセンブラを使用する必要があります。 – Olaf