web-dev-qa-db-fra.com

Est-il possible d'écrire la fonction rapide InvSqrt () de Quake dans Rust?

C'est juste pour satisfaire ma propre curiosité.

Y a-t-il une implémentation de ceci:

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? S'il existe, affichez le code.

J'ai essayé et j'ai échoué. Je ne sais pas comment encoder le nombre flottant en utilisant le format entier. Voici ma tentative:

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
}

Référence:
1. Origine de l'InvSqrt rapide de Quake3 () - Page 1
2. Comprendre la racine carrée inverse rapide de Quake
3. FOT INVERSE SQUARE ROOT.pdf
4. code source: q_math.c # L552-L572

100
Flyq

Je ne sais pas comment encoder le nombre flottant en utilisant le format entier.

Il existe une fonction pour cela: f32::to_bits qui renvoie un u32. Il y a aussi la fonction pour l'autre sens: f32::from_bits qui prend un u32 comme argument. Ces fonctions sont préférées à mem::transmute Car cette dernière est unsafe et délicate à utiliser.

Avec cela, voici l'implémentation de 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)
}

( Aire de jeux )


Cette fonction se compile vers l'assembly suivant sur 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

Je n'ai pas trouvé d'assemblage de référence (si vous en avez, dites-le moi!), Mais cela me semble assez bon. Je ne sais juste pas pourquoi le flotteur a été déplacé dans eax juste pour faire le décalage et la soustraction d'entier. Peut-être que les registres SSE ne prennent pas en charge ces opérations?

clang 9.0 avec -O3 compile le code C en essentiellement le même assemblage . C'est donc un bon signe.


Il convient de souligner que si vous souhaitez réellement l'utiliser dans la pratique: veuillez ne pas le faire. Comme benrg souligné dans les commentaires , les processeurs x86 modernes ont une instruction spécialisée pour cette fonction qui est plus rapide et plus précise que ce hack. Malheureusement, 1.0 / x.sqrt()ne semble pas optimiser cette instruction . Donc, si vous avez vraiment besoin de la vitesse, utiliser les intrinsèques _mm_rsqrt_ps est probablement la voie à suivre. Cependant, cela nécessite à nouveau du code unsafe. Je n'entrerai pas dans les détails de cette réponse, car une minorité de programmeurs en aura réellement besoin.

87
Lukas Kalbertodt

Celui-ci est implémenté avec union moins connu dans 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)
    }
}

A fait quelques micro-tests en utilisant criterion crate sur une boîte Linux x86-64. Étonnamment, la propre sqrt().recip() de Rust est la plus rapide. Mais bien sûr, tout résultat de micro-benchmark doit être pris avec un grain de sel.

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]
37
edwardw

Vous pouvez utiliser std::mem::transmute pour effectuer la conversion nécessaire:

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
}

Vous pouvez rechercher un exemple en direct ici: ici

10
Real Fresh