Apakah mungkin untuk menulis fungsi InvSqrt () cepat Quake di Rust?


101

Ini hanya untuk memuaskan rasa penasaran saya sendiri.

Apakah ada implementasi ini:

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;
}

di Rust? Jika ada, kirim kode.

Saya mencobanya dan gagal. Saya tidak tahu cara menyandikan angka float menggunakan format integer. Ini usaha saya:

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
}

Referensi:
1. Origin of Quake3's Fast InvSqrt () - Halaman 1
2. Memahami Root Inverse Square Cepat Quake
3. ROAST INVESTE SQUARE ROOT.pdf
4. kode sumber: q_math.c # L552-L572



4
Seperti yang saya pahami, kode ini adalah UB dalam C karena melanggar aturan aliasing yang ketat . Cara yang diberkati standar untuk melakukan jenis hukuman seperti ini adalah dengan a union.
trentcl

4
@trentcl: Saya pikir tidak unionberhasil juga. memcpypasti bekerja, meskipun itu verbose.
Matthieu M.

14
@ MatthieuM. Jenis punning dengan serikat adalah C yang benar-benar valid , tetapi tidak valid C ++.
Moira

4
Saya kira pertanyaan ini baik-baik saja dari perspektif keingintahuan murni, tapi tolong mengerti bahwa waktu telah berubah. Pada x86, rsqrtssdan rsqrtpsinstruksi, diperkenalkan dengan Pentium III pada tahun 1999, lebih cepat dan lebih akurat daripada kode ini. NEON ARM memiliki vrsqrteyang serupa. Dan perhitungan apa pun yang digunakan Quake III untuk ini mungkin akan dilakukan pada GPU akhir-akhir ini.
benrg

Jawaban:


87

Saya tidak tahu cara menyandikan angka float menggunakan format integer.

Ada fungsi untuk itu: f32::to_bitsmengembalikan suatu u32. Ada juga fungsi untuk arah lain: f32::from_bitsyang mengambil u32argumen sebagai. Fungsi-fungsi ini lebih disukai daripada mem::transmuteyang terakhir unsafedan sulit untuk digunakan.

Dengan itu, berikut adalah implementasi dari 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)
}

( Taman bermain )


Fungsi ini mengkompilasi ke rakitan berikut pada 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

Saya belum menemukan rakitan referensi (jika ada, tolong beri tahu saya!), Tetapi tampaknya cukup baik bagi saya. Saya hanya tidak yakin mengapa float dipindahkan ke eaxhanya untuk melakukan shift dan pengurangan integer. Mungkin register SSE tidak mendukung operasi itu?

dentang 9.0 dengan -O3mengkompilasi kode C pada dasarnya majelis yang sama . Jadi itu pertanda baik.


Perlu ditunjukkan bahwa jika Anda benar-benar ingin menggunakan ini dalam praktik: tolong jangan. Seperti yang ditunjukkan Benrg dalam komentar , CPU x86 modern memiliki instruksi khusus untuk fungsi ini yang lebih cepat dan lebih akurat daripada peretasan ini. Sayangnya, 1.0 / x.sqrt() sepertinya tidak mengoptimalkan instruksi itu . Jadi, jika Anda benar-benar membutuhkan kecepatan, menggunakan yang _mm_rsqrt_psintrinsik mungkin adalah cara untuk pergi. Namun, ini tidak lagi membutuhkan unsafekode. Saya tidak akan membahas banyak detail dalam jawaban ini, karena sebagian kecil programmer benar-benar membutuhkannya.


4
Menurut Intel Intrinsics Guide tidak ada operasi integer shift yang hanya menggeser 32-bit terendah dari 128-bit register analog ke addssatau mulss. Tetapi jika 96 bit xmm0 lainnya dapat diabaikan maka seseorang dapat menggunakan psrldinstruksi tersebut. Hal yang sama berlaku untuk pengurangan integer.
fsasm

Saya akui tidak tahu apa-apa tentang karat, tetapi bukankah "tidak aman" pada dasarnya adalah properti inti fast_inv_sqrt? Dengan rasa tidak hormat total untuk tipe data dan semacamnya.
Gloweye

12
@Gloweye Ini jenis berbeda "tidak aman" yang kita bicarakan. Perkiraan cepat yang mendapat nilai buruk terlalu jauh dari sweet spot, versus sesuatu yang bermain cepat dan longgar dengan perilaku yang tidak terdefinisi.
Deduplicator

8
@Gloweye: Secara matematis, bagian terakhir dari itu fast_inv_sqrthanyalah satu langkah iterasi Newton-Raphson untuk menemukan perkiraan yang lebih baik inv_sqrt. Tidak ada yang tidak aman tentang bagian itu. Tipuannya ada di bagian pertama, yang menemukan perkiraan yang bagus. Itu bekerja karena ia melakukan pembagian integer oleh 2 pada bagian eksponen float, dan memangsqrt(pow(0.5,x))=pow(0.5,x/2)
MSalters

1
@ fsasm: Benar; movduntuk EAX dan kembali adalah optimasi yang tidak terjawab oleh kompiler saat ini. (Dan ya, konvensi memanggil lulus / kembali skalar floatdalam elemen rendah dari XMM dan memungkinkan bit yang tinggi untuk menjadi sampah Tetapi catatan bahwa jika itu. Itu nol-diperpanjang, dapat dengan mudah tetap seperti itu: pergeseran kanan tidak memperkenalkan non nol elemen dan juga tidak mengurangi _mm_set_epi32(0,0,0,0x5f3759df), yaitu movdbeban. Anda perlu movdqa xmm1,xmm0menyalin reg sebelumnya psrld. Lewati latensi dari penerusan instruksi FP ke integer dan sebaliknya disembunyikan oleh mulsslatensi
Peter Cordes

37

Yang ini diimplementasikan dengan kurang dikenal uniondi Rust:

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)
    }
}

Apakah beberapa tolok ukur mikro menggunakan criterionpeti pada kotak Linux x86-64. Anehnya Rust sendiri sqrt().recip()yang tercepat. Tetapi tentu saja, setiap hasil patokan mikro harus diambil dengan sebutir garam.

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
Saya tidak terkejut sqrt().inv()adalah tercepat. Sqrt dan inv adalah instruksi tunggal hari ini, dan berjalan cukup cepat. Doom ditulis pada hari-hari ketika tidak aman untuk menganggap ada hardware floating point sama sekali, dan fungsi transendental seperti sqrt pasti akan menjadi perangkat lunak. +1 untuk tolok ukur.
Martin Bonner mendukung Monica

4
Apa yang mengejutkan saya adalah bahwa transmuteternyata berbeda dari to_dan from_bits- Saya berharap mereka menjadi setara dengan instruksi bahkan sebelum optimasi.
trentcl

2
@ MartinBonner (Juga, bukan itu penting, tapi sqrt bukan fungsi transendental .)
benrg

4
@ MartinBonner: FPU perangkat keras apa pun yang mendukung divisi biasanya juga akan mendukung sqrt. Operasi "dasar" IEEE (+ - * / sqrt) diperlukan untuk menghasilkan hasil yang dibulatkan dengan benar; itu sebabnya SSE menyediakan semua operasi itu tetapi tidak exp, dosa, atau apa pun. Bahkan, divide dan sqrt biasanya dijalankan pada unit eksekusi yang sama, dirancang dengan cara yang sama. Lihat detail unit HW div / sqrt . Bagaimanapun, mereka masih tidak cepat dibandingkan dengan berkembang biak, terutama dalam latensi.
Peter Cordes

1
Bagaimanapun, Skylake memiliki pipelining yang jauh lebih baik untuk div / sqrt daripada uarches sebelumnya. Lihat divisi Floating point vs multiplication floating point untuk beberapa ekstrak dari tabel Agner Fog. Jika Anda tidak melakukan banyak pekerjaan lain dalam satu lingkaran sehingga sqrt + div adalah hambatan, Anda mungkin ingin menggunakan HW cepat timbal balik sqrt (bukan retas gempa) + a iterasi Newton. Apalagi dengan FMA yang bagus untuk throughput, kalau bukan latensi. Rsqrt vektor cepat dan timbal balik dengan SSE / AVX tergantung pada presisi
Peter Cordes

10

Anda dapat menggunakan std::mem::transmuteuntuk membuat konversi yang dibutuhkan:

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
}

Anda dapat mencari contoh langsung di sini: di sini


4
Tidak ada yang salah dengan tidak aman, tetapi ada cara untuk melakukan ini tanpa blok tidak aman eksplisit, jadi saya sarankan untuk menulis ulang jawaban ini menggunakan f32::to_bitsdan f32::from_bits. Itu juga membawa maksud jelas tidak seperti transmutasi, yang kebanyakan orang mungkin melihat sebagai "sihir".
Sahsahae

5
@Sahsahae Saya baru saja mengirim jawaban menggunakan dua fungsi yang Anda sebutkan :) Dan saya setuju, unsafeharus dihindari di sini, karena itu tidak perlu.
Lukas Kalbertodt
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.