RustでQuakeの高速InvSqrt()関数を書くことは可能ですか?


101

これは私自身の好奇心を満たすためです。

これの実装はありますか:

float InvSqrt (float x)
{
   float xhalf = 0.5f*x;
   int i = *(int*)&x;
   i = 0x5f3759df - (i>>1);
   x = *(float*)&i;
   x = x*(1.5f - xhalf*x*x);
   return x;
}

ルストで?存在する場合は、コードを投稿してください。

私はそれを試して失敗しました。整数形式を使用して浮動小数点数をエンコードする方法がわかりません。これが私の試みです:

fn main() {
    println!("Hello, world!");
    println!("sqrt1: {}, ",sqrt2(100f64));
}

fn sqrt1(x: f64) -> f64 {
    x.sqrt()
}

fn sqrt2(x: f64) -> f64 {
    let mut x = x;
    let xhalf = 0.5*x;
    let mut i = x as i64;
    println!("sqrt1: {}, ", i);

    i = 0x5f375a86 as i64 - (i>>1);

    x = i as f64;
    x = x*(1.5f64 - xhalf*x*x);
    1.0/x
}

参照:
1. Quake3の高速InvSqrt()の起源-ページ1
2. Quakeの高速逆平方根について
3. 高速逆平方根.pdf
4. ソースコード:q_math.c#L552-L572



4
私が理解しているように、このコードは厳密なエイリアシングルールに違反しているため、CのUB です。このようなタイプのパンニングを実行する標準的な方法は、を使用することunionです。
trentcl

4
@trentcl:私もうまくいくとは思いませんunionmemcpy冗長ですが、間違いなく動作します。
Matthieu M.

14
@MatthieuM。共用体による型パンニングは完全に有効なCですが、有効なC ++ではありません。
モイラ

4
この質問は純粋な好奇心の観点からは問題ないと思いますが、時代が変わったことをご理解ください。x86では、1999年にPentium IIIで導入されたrsqrtssおよびrsqrtps命令は、このコードよりも高速で正確です。ARM NEONにはvrsqrteどちらが似ています。そして、Quake IIIがこれを使用した計算は、いずれにせよ最近のGPUでおそらく行われるでしょう。
benrg

回答:


87

整数形式を使用して浮動小数点数をエンコードする方法がわかりません。

そのための関数がありf32::to_bitsますu32。これはを返します。他の方向の関数もあります。f32::from_bitsこれは、u32引数として使用します。mem::transmute後者の方がunsafe使いやすく、扱いにくいので、これらの関数は優先されます。

それで、これはの実装ですInvSqrt

fn inv_sqrt(x: f32) -> f32 {
    let i = x.to_bits();
    let i = 0x5f3759df - (i >> 1);
    let y = f32::from_bits(i);

    y * (1.5 - 0.5 * x * y * y)
}

遊び場


この関数は、x86-64で次のアセンブリにコンパイルされます。

.LCPI0_0:
        .long   3204448256        ; f32 -0.5
.LCPI0_1:
        .long   1069547520        ; f32  1.5
example::inv_sqrt:
        movd    eax, xmm0
        shr     eax                   ; i << 1
        mov     ecx, 1597463007       ; 0x5f3759df
        sub     ecx, eax              ; 0x5f3759df - ...
        movd    xmm1, ecx
        mulss   xmm0, dword ptr [rip + .LCPI0_0]    ; x *= 0.5
        mulss   xmm0, xmm1                          ; x *= y
        mulss   xmm0, xmm1                          ; x *= y
        addss   xmm0, dword ptr [rip + .LCPI0_1]    ; x += 1.5
        mulss   xmm0, xmm1                          ; x *= y
        ret

参照アセンブリは見つかりませんでした(もしあれば、教えてください!)が、私にはかなり良いようです。eaxシフトと整数の減算を行うためだけにフロートが移動された理由がわかりません。たぶん、SSEレジスタはこれらの操作をサポートしていませんか?

clang 9.0は-O3、Cコードを基本的に同じアセンブリにコンパイルします。これは良い兆候です。


これを実際に使用したい場合は、使用しないでください。benrg がコメント指摘したように、最近のx86 CPUには、このハックよりも高速で正確なこの機能のための専用の命令があります。残念ながら、1.0 / x.sqrt() はその命令に最適化されていないようです。したがって、本当に速度が必要な場合は_mm_rsqrt_ps組み込み関数を使用するのがよいでしょう。ただし、これにはunsafeコードが必要です。少数のプログラマーが実際にそれを必要とするので、この回答については詳しく説明しません。


4
インテルイントリンシクスガイドによると、128ビットレジスタの最下位32ビットをアナログaddssまたはにシフトするだけの整数シフト演算はありませんmulss。しかし、xmm0の他の96ビットを無視できる場合は、そのpsrld命令を使用できます。整数の減算についても同様です。
fsasm

私は錆についてほとんど何も知らないことを認めますが、「安全でない」は基本的にfast_inv_sqrtのコアプロパティではありませんか?データ型などを完全に軽視しています。
グローアイ

12
@Gloweyeそれは私たちが話している別のタイプの「危険」です。未定義の動作で速くて緩いものに対して、スイートスポットから離れすぎた悪い値を取得する高速近似。
Deduplicator

8
@Gloweye:数学的には、その最後の部分はfast_inv_sqrt、のより良い近似を見つけるための1つのニュートンラフソン反復ステップですinv_sqrt。その部分には何も危険はありません。トリックは最初の部分にあり、良い近似を見つけます。これは、浮動小数点の指数部で2による整数除算を行っているため機能します。実際、sqrt(pow(0.5,x))=pow(0.5,x/2)
MSalters

1
@fsasm:その通りです。movdEAXに戻ると、現在のコンパイラでは最適化が行われません。(そして、はい、呼び出し規約floatはXMMの低要素でスカラーをパス/リターンし、高ビットをガベージにすることを許可します。しかし、それゼロ拡張された場合、それは簡単にその方法にとどまることができます:右シフトは非ゼロ要素とどちらの減算を行いますから_mm_set_epi32(0,0,0,0x5f3759df)、すなわちmovd負荷あなたが必要となる。movdqa xmm1,xmm0前にREGをコピーするpsrldバイパスレイテンシーFP命令の転送の整数と副にその逆によって隠されている。mulssレイテンシー。
ピーター・コルド

37

これはunionRust であまり知られていない実装です:

union FI {
    f: f32,
    i: i32,
}

fn inv_sqrt(x: f32) -> f32 {
    let mut u = FI { f: x };
    unsafe {
        u.i = 0x5f3759df - (u.i >> 1);
        u.f * (1.5 - 0.5 * x * u.f * u.f)
    }
}

criterionx86-64 Linuxボックスでクレートを使用していくつかのマイクロベンチマークを行いました。驚いたことに、Rustのものsqrt().recip()は最速です。ただし、もちろん、すべてのマイクロベンチマーク結果は、1粒の塩で取得する必要があります。

inv sqrt with transmute time:   [1.6605 ns 1.6638 ns 1.6679 ns]
inv sqrt with union     time:   [1.6543 ns 1.6583 ns 1.6633 ns]
inv sqrt with to and from bits
                        time:   [1.7659 ns 1.7677 ns 1.7697 ns]
inv sqrt with powf      time:   [7.1037 ns 7.1125 ns 7.1223 ns]
inv sqrt with sqrt then recip
                        time:   [1.5466 ns 1.5488 ns 1.5513 ns]

22
驚いたことはありませんが、sqrt().inv()最速です。最近ではsqrtとinvの両方が単一の命令であり、かなり高速に進んでいます。Doomは、ハードウェアの浮動小数点がまったくないと想定するのが安全ではなかった時代に書かれたもので、sqrtのような超越関数は間違いなくソフトウェアでした。ベンチマークの+1。
Martin Bonnerがモニカをサポートする

4
どのような私を驚かすることはつまり、transmute明らかに異なっているto_from_bits私はそれらにも、最適化の前に命令-同等であることを期待したいです- 。
trentcl

2
@MartinBonner(また、重要ではありませんが、sqrtは超越関数ではありません。)
benrg

4
@MartinBonner:通常、除算をサポートするハードウェアFPUはsqrtもサポートします。正しく丸められた結果を生成するには、IEEEの「基本」操作(+-* / sqrt)が必要です。そのため、SSEはこれらの操作をすべて提供しますが、exp、sinなどは提供しません。実際、divideとsqrtは通常、同じように設計された同じ実行ユニットで実行されます。参照HW DIV / SQRTユニットの詳細。とにかく、特にレイテンシの点で、乗算に比べて高速ではありません。
Peter Cordes

1
とにかく、Skylakeのdiv / sqrtのパイプラインは、以前のバージョンよりもはるかに優れています。Agner Fogの表からの一部の抜粋については、浮動小数点除算と浮動小数点乗算を参照してください。ループで他の多くの作業を行っていないためsqrt + divがボトルネックになっている場合は、(地震ハックの代わりに)HW高速相互sqrt +ニュートン反復を使用することをお勧めします。特に、レイテンシではないにしても、スループットに優れたFMAを使用します。 高速ベクトル化されたrsqrtと精度に応じたSSE / AVXとの相互関係
Peter Cordes

10

std::mem::transmute必要な変換を行うために使用できます:

fn inv_sqrt(x: f32) -> f32 {
    let xhalf = 0.5f32 * x;
    let mut i: i32 = unsafe { std::mem::transmute(x) };
    i = 0x5f3759df - (i >> 1);
    let mut res: f32 = unsafe { std::mem::transmute(i) };
    res = res * (1.5f32 - xhalf * res * res);
    res
}

ここでライブの例を探すことができます:ここに


4
安全でないことには何の問題もありませんが、明示的な安全でないブロックなしでこれを行う方法があるので、f32::to_bitsおよびを使用してこの回答を書き直すことをお勧めしf32::from_bitsます。それはまた、ほとんどの人がおそらく「魔法」と見なすトランスミュートとは異なり、その意図を明らかに持っています。
Sahsahae

5
@Sahsahae私はあなたが言及した2つの関数を使用して回答を投稿しました:)そして、私は同意する、unsafeそれは必要ではないので、ここでは避けるべきです。
Lukas Kalbertodt
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.