È possibile scrivere la funzione invSqrt () di Quake in Rust?


101

Questo è solo per soddisfare la mia curiosità.

C'è un'implementazione di questo:

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

in ruggine? Se esiste, pubblica il codice.

L'ho provato e non ci sono riuscito. Non so come codificare il numero float usando il formato intero. Ecco il mio tentativo:

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
}

Riferimento:
1. Origine di Fast InvSqrt di Quake3 () - Pagina 1
2. Comprensione della radice quadrata inversa veloce di Quake
3. FAST INVERSE SQUARE ROOT.pdf
4. codice sorgente: q_math.c # L552-L572



4
A quanto ho capito, questo codice è UB in C a causa della violazione della rigida regola di aliasing . Il modo benedetto standard per eseguire questo tipo di punzonatura di tipo è con a union.
trentcl,

4
@trentcl: non penso che funzioni unionneanche. memcpysicuramente funziona, anche se è prolisso.
Matthieu M.,

14
@MatthieuM. La punzonatura con i sindacati è perfettamente valida C , ma non valida C ++.
Moira,

4
Suppongo che questa domanda vada bene da una prospettiva di pura curiosità, ma per favore comprendi che i tempi sono cambiati. Su x86, le istruzioni rsqrtsse rsqrtps, introdotte con Pentium III nel 1999, sono più veloci e più accurate di questo codice. ARM NEON ha vrsqrtequale è simile. E qualunque sia il calcolo utilizzato da Quake III per questo, probabilmente verrebbe comunque eseguito sulla GPU in questi giorni.
ben

Risposte:


87

Non so come codificare il numero float usando il formato intero.

C'è una funzione per questo: f32::to_bitsche restituisce un u32. C'è anche la funzione per l'altra direzione: f32::from_bitsche accetta un u32argomento come. Queste funzioni sono preferite rispetto mem::transmutea quest'ultimaunsafe e difficili da usare.

Con ciò, ecco l'implementazione di 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)
}

( Parco giochi )


Questa funzione viene compilata nel seguente assembly su 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

Non ho trovato alcun assemblaggio di riferimento (se sì, per favore dimmelo!), Ma mi sembra abbastanza buono. Non sono sicuro del perché il float sia stato spostatoeax solo per fare la sottrazione di spostamento e numero intero. Forse i registri SSE non supportano tali operazioni?

clang 9.0 con -O3compila il codice C sostanzialmente nello stesso assembly . Quindi questo è un buon segno.


Vale la pena sottolineare che se in realtà si desidera utilizzare questo in pratica: per favore non farlo. Come sottolineato da Benrg nei commenti , le moderne CPU x86 hanno un'istruzione specializzata per questa funzione che è più veloce e più accurata di questo hack. Sfortunatamente, 1.0 / x.sqrt() non sembra ottimizzare a tale istruzione . Quindi, se hai davvero bisogno della velocità, usare l' _mm_rsqrt_psintrinseca è probabilmente la strada da percorrere. Ciò, tuttavia, richiede nuovamente il unsafecodice. Non entrerò nei dettagli in questa risposta, in quanto una minoranza di programmatori ne avrà effettivamente bisogno.


4
Secondo la Intel Intrinsics Guide non esiste alcuna operazione di spostamento dei numeri interi che sposta solo il 32-bit più basso dell'analogo del registro a 128-bit su addsso mulss. Ma se gli altri 96 bit di xmm0 possono essere ignorati, si potrebbe usare l' psrldistruzione. Lo stesso vale per la sottrazione di numeri interi.
fsasm,

Devo ammettere di non sapere quasi nulla della ruggine, ma non è "insicuro" fondamentalmente una proprietà chiave di fast_inv_sqrt? Con la sua totale mancanza di rispetto per i tipi di dati e simili.
Gloweye,

12
@Gloweye È un diverso tipo di "non sicuro" di cui parliamo però. Un'approssimazione veloce che ottiene un valore negativo troppo lontano dal punto debole, rispetto a qualcosa che gioca veloce e sciolto con un comportamento indefinito.
Deduplicatore,

8
@Gloweye: matematicamente, l'ultima parte fast_inv_sqrtè solo un passaggio di iterazione di Newton-Raphson per trovare una migliore approssimazione di inv_sqrt. Non c'è nulla di pericoloso in quella parte. L'inganno è nella prima parte, che trova una buona approssimazione. Funziona perché sta facendo una divisione intera per 2 sulla parte esponente del float, e in effettisqrt(pow(0.5,x))=pow(0.5,x/2)
MSalters

1
@fsasm: è corretto; movdper EAX e ritorno è una mancata ottimizzazione da parte degli attuali compilatori. (E sì, convenzioni di chiamata passano / scalare ritorno floatnell'elemento bassa di un XMM e permettono elevati bit da spazzatura Ma si noti che se si. Era zero esteso, si può facilmente rimanere in questo modo: spostamento a destra non introduce non- zero elementi e nessuna sottrazione da _mm_set_epi32(0,0,0,0x5f3759df), cioè un movdcarico. Avresti bisogno di una movdqa xmm1,xmm0copia prima del reg psrld. Ignora la latenza dall'inoltro dell'istruzione FP all'intero e viceversa è nascosta dalla mulsslatenza
Peter Cordes

37

Questo è implementato con meno conosciuto unionin 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)
    }
}

Alcuni micro benchmark hanno usato la criterioncassa su una scatola Linux x86-64. Sorprendentemente, quello di Rust sqrt().recip()è il più veloce. Ma ovviamente, qualsiasi risultato di micro benchmark dovrebbe essere preso con un granello di sale.

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
Non sono minimamente sorpreso sqrt().inv()è il più veloce. Oggigiorno sia sqrt che inv sono singole istruzioni e vanno abbastanza velocemente. Doom è stato scritto ai tempi in cui non era sicuro supporre che ci fosse del tutto in virgola mobile hardware e le funzioni trascendentali come sqrt sarebbero state sicuramente software. +1 per i benchmark.
Martin Bonner supporta Monica il

4
Ciò che mi sorprende è che transmuteè apparentemente diverso da to_e from_bits- mi aspetto che siano equivalenti alle istruzioni anche prima dell'ottimizzazione.
trentcl,

2
@MartinBonner (Inoltre, non è importante, ma sqrt non è una funzione trascendentale .)
ben

4
@MartinBonner: qualsiasi FPU hardware che supporti la divisione normalmente supporterà anche sqrt. Le operazioni IEEE "di base" (+ - * / sqrt) sono necessarie per produrre un risultato correttamente arrotondato; ecco perché SSE fornisce tutte quelle operazioni ma non exp, sin o altro. In effetti, divide e sqrt in genere vengono eseguiti sulla stessa unità di esecuzione, progettata in modo simile. Vedi i dettagli dell'unità div / sqrt HW . Ad ogni modo, non sono ancora veloci rispetto al moltiplicarsi, specialmente in latenza.
Peter Cordes,

1
Ad ogni modo, Skylake ha una pipeline significativamente migliore per div / sqrt rispetto ai precedenti Uarca. Vedi Divisione in virgola mobile vs moltiplicazione in virgola mobile per alcuni estratti della tabella della nebbia di Agner. Se non stai facendo molto altro lavoro in un ciclo, quindi sqrt + div è un collo di bottiglia, potresti voler usare HW veloce reciproco sqrt (invece dell'hack di terremoto) + una iterazione di Newton. Soprattutto con FMA che è buono per la velocità effettiva, se non la latenza. Risposta rapida vettorializzata e reciproca con SSE / AVX a seconda della precisione
Peter Cordes

10

È possibile utilizzare std::mem::transmuteper effettuare la conversione necessaria:

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
}

Puoi cercare un esempio dal vivo qui: qui


4
Non c'è nulla di sbagliato in unsafe, ma c'è un modo per farlo senza un blocco esplicito e non sicuro, quindi suggerirei di riscrivere questa risposta usando f32::to_bitse f32::from_bits. Presenta anche l'intenzione chiaramente diversa dalla trasmutazione, che la maggior parte delle persone probabilmente considera "magica".
Sahsahae,

5
@Sahsahae Ho appena pubblicato una risposta utilizzando le due funzioni che hai menzionato :) E sono d'accordo, unsafedovrebbe essere evitato qui, poiché non è necessario.
Lukas Kalbertodt,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.