Rust'un Quake'in hızlı InvSqrt () fonksiyonunu yazmak mümkün müdür?


101

Bu sadece kendi merakımı tatmin etmek için.

Bunun bir uygulaması var mı:

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

Rust bölgesinde bulundunuz mu? Varsa, kodu gönderin.

Denedim ve başarısız oldum. Tamsayı biçimini kullanarak kayan nokta numarasını nasıl kodlayacağımı bilmiyorum. İşte benim girişimim:

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
}

Referans:
1. Quake3'ün Hızlı InvSqrt () kaynağının kökeni () - Sayfa 1
2. Quake'in Hızlı Ters Kare Kökünü Anlama
3. HIZLI İNVERSE KARE ROOT.pdf
4. kaynak kodu: q_math.c # L552-L572



4
Anladığım kadarıyla, bu kod sıkı bir takma adlandırma kuralını ihlal ettiği için C'de UB'dir . Bu tür punning yapmak için standart mübarek yol ile a union.
trentcl

4
@trentcl: Ben de unionişe yaradığını sanmıyorum . memcpykesinlikle işe yarıyor, buna rağmen ayrıntılı.
Matthieu

14
@MatthieuM. Rakorlar ile punning tipi mükemmel şekilde geçerli C , ancak geçerli C ++ değil.
Moira

4
Bu sorunun saf bir merak açısından iyi olduğunu düşünüyorum, ancak zamanların değiştiğini lütfen anlayın. X86'da, 1999'da Pentium III ile sunulan rsqrtssve rsqrtpstalimatları bu koddan daha hızlı ve daha doğrudur. ARM NEON vrsqrteda buna benzer. Ve Quake III'ün bunu kullandığı hesaplamalar ne olursa olsun, muhtemelen bugünlerde GPU'da yapılacaktı.
benrg

Yanıtlar:


87

Tamsayı biçimini kullanarak kayan nokta numarasını nasıl kodlayacağımı bilmiyorum.

Bunun için bir fonksiyon vardır: f32::to_bitsbir u32. Diğer yönün işlevi de vardır: f32::from_bitsbir u32argüman olarak alır . Bu işlevler mem::transmute, ikincisi unsafeve kullanımı zor olduğu için tercih edilir .

Bununla, burada uygulanması 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)
}

( Oyun Parkı )


Bu işlev x86-64 üzerinde aşağıdaki derlemeyi derler:

.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

Herhangi bir referans meclisi bulamadım (eğer varsa, lütfen söyle!), Ama benim için oldukça iyi görünüyor. Ben sadece eaxkayma ve tamsayı çıkarma yapmak için şamandıra neden taşındı emin değilim . Belki SSE kayıtları bu işlemleri desteklemiyor olabilir?

clang 9.0 with -O3C kodunu temelde aynı montaj için derler . Yani bu iyi bir işaret.


Bunu aslında pratikte kullanmak istiyorsanız: lütfen kullanmayın. Benrg'nin yorumlarda belirttiği gibi , modern x86 CPU'lar bu işlev için bu hack'ten daha hızlı ve daha doğru olan özel bir talimata sahiptir. Ne yazık ki, 1.0 / x.sqrt() bu talimata optimize edilmiş görünmüyor . Bu yüzden gerçekten hıza ihtiyacınız varsa , _mm_rsqrt_psintrinsikleri kullanmak muhtemelen yoludur. Ancak bu yine unsafekod gerektirir . Bu cevapta fazla ayrıntıya girmeyeceğim, çünkü az sayıda programcı aslında buna ihtiyaç duyacak.


4
Intel Intrinsics Guide'a göre, yalnızca 128-bit kayıt analoğunun en düşük 32-bit'ini addssveya değerine değiştiren tamsayı kaydırma işlemi yoktur mulss. Ancak xmm0'ın diğer 96 biti göz ardı edilebilirse, psrldtalimat kullanılabilir. Tamsayı çıkarma için de aynı şey geçerlidir.
fsasm

Ben pas hakkında hiçbir şey yanında bilmek kabul, ama "güvensiz" temelde fast_inv_sqrt bir temel özelliği değil mi? Veri türleri ve benzeri için tamamen saygısızlık ile.
Gloweye

12
@Gloweye Bahsettiğimiz farklı bir tür "güvensiz". Tanımlanamayan davranışlarla hızlı ve gevşek oynayan bir şeye karşı, tatlı noktadan çok kötü bir değer alan hızlı bir yaklaşım.
Tekilleştirici

8
@Gloweye: Matematiksel olarak, bunun son kısmı fast_inv_sqrtdaha iyi bir yaklaşım bulmak için sadece bir Newton-Raphson yineleme adımıdır inv_sqrt. Bu kısımda güvensiz bir şey yok. Hile ilk bölümde, bu da iyi bir yaklaşım buluyor. Bu işe yarıyor çünkü şamandıranın üst kısmında 2'ye bir tamsayı bölme yapıyor ve gerçekten desqrt(pow(0.5,x))=pow(0.5,x/2)
MSalters

1
@fsasm: Doğru; movdEAX ve back, mevcut derleyiciler tarafından kaçırılan bir optimizasyon. (Ve evet, arama kuralları / dönüş skaler geçmesi floato eğer bir XMM düşük elemanda ve yüksek bit çöp olmasına izin Ama senedin. İdi sıfır genişletilmiş, kolayca bu şekilde kalabilirler: Sağ değişen olmayan tanıtmak gelmez sıfır elemanları ve ne gelen çıkarma yok _mm_set_epi32(0,0,0,0x5f3759df), yani bir movdyük bir gerekir. movdqa xmm1,xmm0önce reg kopyalamak psrldBaypas gecikme FP talimat yönlendirmeden kaynaklanan tamsayı ve tersi olarak gizlidir için. mulssgecikme.
Peter Cordes

37

Bu, Rust'da daha az bilinen ile uygulanır union:

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

Kullanarak bazı mikro kriterler yaptı mı criterionBir x86-64 Linux kutusunda sandık . Şaşırtıcı bir şekilde Rust'un kendisi sqrt().recip()en hızlısıdır. Ancak elbette, herhangi bir mikro kıyaslama sonucu bir tuz tanesi ile alınmalıdır.

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
Ben en az şaşırmadım sqrt().inv()en hızlıdır. Hem sqrt hem de inv bugünlerde tek talimatlar ve oldukça hızlı gidin. Doom, donanım kayan nokta olduğunu varsaymanın güvenli olmadığı günlerde yazılmıştır ve sqrt gibi aşkın işlevler kesinlikle yazılım olurdu . Kıyaslamalar için +1.
Martin Bonner Monica'yı

4
Beni şaşırtan şey, transmutegörünüşe göre farklı to_ve from_bits- optimizasyondan önce bile bunların talimat eşdeğeri olmasını beklerdim.
trentcl

2
@MartinBonner (Ayrıca, önemli değil, ama sqrt aşkın bir işlev değildir .)
benrg

4
@MartinBonner: Bölmeyi destekleyen tüm donanım FPU'ları normalde sqrt'ı da destekler. Doğru şekilde yuvarlanmış bir sonuç elde etmek için IEEE "temel" işlemleri (+ - * / sqrt) gerekir; bu yüzden SSE exp, sin ya da her neyse, tüm bu işlemleri sağlar. Aslında, bölme ve sqrt genellikle benzer bir şekilde tasarlanmış aynı yürütme biriminde çalışır. Bkz HW div / sqrt birim ayrıntıları . Her neyse, çoğalmaya kıyasla hala hızlı değiller, özellikle gecikmede.
Peter Cordes

1
Her neyse, Skylake div / sqrt için önceki uarch'lardan önemli ölçüde daha iyi boru hattına sahiptir. Agner Sis tablosundaki bazı alıntılar için Kayan nokta bölümü ve kayan nokta çarpımı konusuna bakın . Bir döngüde başka bir iş yapmıyorsanız, sqrt + div bir darboğazdır, HW hızlı karşılıklı sqrt (deprem kesmek yerine) + Newton yinelemesi kullanmak isteyebilirsiniz. Özellikle gecikme olmasa bile verim için iyi olan FMA ile. Hassaslığa bağlı olarak SSE / AVX ile hızlı vektörize rsqrt ve karşılıklı
Peter Cordes

10

std::mem::transmuteGerekli dönüşümü yapmak için kullanabilirsiniz :

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
}

Burada canlı bir örnek arayabilirsiniz: burada


4
Güvensiz ile ilgili yanlış bir şey yok, ancak bunu açık güvensiz blok olmadan yapmanın bir yolu var, bu yüzden f32::to_bitsve ile bu yanıtı yeniden yazmanızı öneririm f32::from_bits. Ayrıca, çoğu insanın muhtemelen "sihir" olarak baktığı dönüştürmeden farklı olarak niyeti taşır.
Sahsahae

5
@Sahsahae Az önce bahsettiğiniz iki işlevi kullanarak bir cevap gönderdim :) Ve katılıyorum, unsafeburada kaçınılmalıdır, gerekli olmadığı için.
Lukas Kalbertodt
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.