doubleを32ビットintに丸める高速な方法の説明


169

Luaのソースコードを読んだとき、Luaはa macroを使用してa doubleを32ビットに丸めることに気付きましたint。を抽出するmacroと、次のようになります。

union i_cast {double d; int i[2]};
#define double2int(i, d, t)  \
    {volatile union i_cast u; u.d = (d) + 6755399441055744.0; \
    (i) = (t)u.i[ENDIANLOC];}

ここでENDIANLOCは、リトルエンディアン、ビッグエンディアンのエンディアンネスとして定義されています。Luaはエンディアンを慎重に処理します。またはのような整数型を表します。01tintunsigned int

私は少し調査しましたがmacro、同じ考えを使用するより簡単な形式があります。

#define double2int(i, d) \
    {double t = ((d) + 6755399441055744.0); i = *((int *)(&t));}

またはC ++スタイルで:

inline int double2int(double d)
{
    d += 6755399441055744.0;
    return reinterpret_cast<int&>(d);
}

このトリックは、IEEE 754を使用するどのマシンでも機能します(つまり、今日のほとんどすべてのマシンを意味します)。これは正の数と負の数の両方で機能し、丸めはバンカーの規則に従います。(IEEE 754に準拠しているため、これは驚くべきことではありません。)

私はそれをテストする小さなプログラムを書きました:

int main()
{
    double d = -12345678.9;
    int i;
    double2int(i, d)
    printf("%d\n", i);
    return 0;
}

期待どおりに-12345679を出力します。

このトリッキーなmacroしくみについて詳しく説明します。マジックナンバー6755399441055744.0は実際には2^51 + 2^52、または1.5 * 2^52であり1.5、バイナリではと表すことができます1.1。このマジック番号に32ビット整数が追加されると、まあ、私はここから失われます。このトリックはどのように機能しますか?

PS:これはLuaのソースコードLlimits.hにあります。

更新

  1. @Mysticialが指摘するように、このメソッドはそれ自体を32ビットintに制限せずint、数値が2 ^ 52の範囲内にある限り、64ビットに拡張することもできます。(にmacroはいくつかの変更が必要です。)
  2. 一部の資料では、この方法はDirect3Dでは使用できないとしています。
  3. x86用のMicrosoftアセンブラーを使用すると、さらに高速にmacro記述されますassembly(これもLuaソースから抽出されます)。

    #define double2int(i,n)  __asm {__asm fld n   __asm fistp i}
  4. 単精度数にも同様のマジックナンバーがあります。 1.5 * 2 ^23


3
何と比べて「速い」?
Cory Nelson

3
@CoryNelson単純なキャストと比較して高速。このメソッドを(SSE組み込み関数を使用して)正しく実装すると、文字通りキャストよりも100倍高速になります。(かなり高価な変換コードへの厄介な関数呼び出しを呼び出す)
Mysticial

2
正しい-私はそれがより速いのを見ることができますftoi。しかし、SSEについて話している場合は、単一の命令を使用しないのはなぜCVTTSD2SIですか?
Cory Nelson

3
@tmyklebu行く多くのユースケースdouble -> int64は確かに2^52範囲内です。これらは、浮動小数点FFTを使用して整数たたみ込みを実行する場合に特に一般的です。
Mysticial 2013年

7
@MSalters必ずしも正しいとは限りません。キャストは、オーバーフローやNANケースの適切な処理を含め、言語の仕様に準拠している必要があります。(またはコンパイラーがIBまたはUBの場合に指定するもの)これらのチェックは非常に高価になる傾向があります。この質問で言及されているトリックは、そのようなコーナーケースを完全に無視しています。したがって、速度が必要で、アプリケーションがそのようなコーナーケースを気にしない(または遭遇しない)場合、このハックは完全に適切です。
Mysticial 2013年

回答:


161

A doubleは次のように表されます。

二重表現

そして、それは2つの32ビット整数として見ることができます。現在、intコードのすべてのバージョンで使用されているもの(32ビットであると想定int)は、図の右側にあるため、最終的に実行しているのは、仮数の最下位32ビットを使用しているだけです。


さて、マジックナンバーへ。あなたが正しく述べたように、6755399441055744は2 ^ 51 + 2 ^ 52です。そのような数を追加doubleすると、は2 ^ 52と2 ^ 53の間の「甘い範囲」に入るように強制されます。これは、Wikipediaによってここで説明されているように、興味深い特性を持っています。

2 52 = 4,503,599,627,370,496と2 53 = 9,007,199,254,740,992の間では、表現可能な数値は正確に整数です

これは、仮数が52ビット幅であるという事実から来ています。

2 51 +2 52を追加することに関する他の興味深い事実は、2つの最上位ビットでのみ仮数に影響を与えることです。これは、最下位の32ビットのみを使用するため、とにかく破棄されます。


最後になりましたが、サイン。

IEEE 754浮動小数点はマグニチュードと符号の表現を使用しますが、「通常の」マシンの整数は2の補数演算を使用します。ここでこれはどのように処理されますか?

正の整数についてのみ話しました。ここで、32ビットintで表現可能な範囲の負の数を扱っていると仮定します。そのため、(-2 ^ 31 + 1)よりも(絶対値で)少なくなります。それを呼ぶ-a。このような数値は、マジックナンバーを追加することで明らかに正になり、結果の値は2 52 +2 51 +(-a)になります。

では、仮数を2の補数表現で解釈するとどうなるでしょうか。(2 52 +2 51)と(-a)の2の補数の合計の結果である必要があります。この場合も、最初の項は上位2ビットにのみ影響し、ビット0〜50に残るのは(-a)の2の補数表現です(ここでも、上位2ビットを引いたものです)。

2の補数をより小さな幅に縮小するには、左側の余分なビットを切り捨てるだけなので、下位の32ビットを使用すると、32ビットの2の補数演算で正しく(-a)が得られます。


"" "2 ^ 51 + 2 ^ 52を追加することに関する他の興味深い事実は、2つの最上位ビットでのみ仮数に影響を与えることです-最下位の32ビットのみを取得するため、とにかく破棄されます" ""何ですか?これを追加すると、すべての仮数がシフトする可能性があります。
YvesgereY 2013年

@ジョン:もちろん、それらを追加する全体のポイントは、値を強制的にその範囲内にすることです。これにより、元の値に対して仮数が(他のものの間で)シフトする可能性があります。ここで私が言っていたのは、いったんその範囲に入ると、対応する53ビット整数と異なるビットはビット51と52だけであり、いずれにしても破棄されるということです。
Matteo Italia

2
変換したい人のためにint64_t、仮数を左にシフトしてから13ビット右にシフトすることでそれを行うことができます。これにより、「マジック」番号から指数と2ビットがクリアされますが、符号は保持され、64ビット符号付き整数全体に伝搬されます。union { double d; int64_t l; } magic; magic.d = input + 6755399441055744.0; magic.l <<= 13; magic.l >>= 13;
Wojciech Migda 2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.