2012-03-30 26 views
17

C'è un modo per migliorare il reciproco (divisione 1 su X) rispetto alla velocità, se la precisione non è cruciale?Divisione veloce 1/X (reciproco)

Quindi, ho bisogno di calcolare 1/X. C'è qualche soluzione, quindi perdo la precisione ma lo faccio più velocemente?

+3

Questo dipende in gran parte dalla piattaforma hardware su cui stai lavorando. Inoltre, dipende anche da quanto precisione sei disposto a perdere. Ovviamente, 'float recip (float x) {return 1; } 'è molto veloce, ma non molto preciso ... –

+8

[I reciproci a precisione singola vengono eseguiti in 5 cicli sui processori più recenti. Una moltiplicazione in virgola mobile è anche di 5 cicli.] (Http://www.agner.org/optimize/instruction_tables.pdf) Quindi dubito seriamente che otterrai qualcosa di più veloce di qualcosa come '(float) 1/(float) x'. – Mysticial

+2

Per i principianti, qual è la tua piattaforma e il tuo compilatore? E che tipo di dati stai operando? –

risposta

7

Innanzitutto, assicurarsi che questo non sia un caso di ottimizzazione prematura. Sai che questo è il collo di bottiglia?

Come dice Mystical, 1/x può essere calcolato molto rapidamente. Assicurati di non utilizzare il tipo di dati double per 1 o il divisore. I galleggianti sono molto più veloci.

Detto questo, benchmark, benchmark, benchmark. Non sprecare il tuo tempo a spendere ore in teoria numerica solo per scoprire la fonte della scarsa performance è l'accesso IO.

+2

"I float sono molto più veloci" - davvero? È pericoloso fare affermazioni così radicali. Ci sono molte cose che puoi fare per cambiare il codice generato dal compilatore. Dipende anche dall'hardware a cui mira il compilatore. Ad esempio, su IA32, il codice generato da gcc quando non si utilizza SSE (l'opzione -mfpmath = 387 penso) sarà la stessa velocità per double e float poiché la FPU si occupa solo di valori a 80 bit, qualsiasi differenza di velocità sarà ridotta alla larghezza di banda della memoria. – Skizz

+1

Sì, ovviamente è una dichiarazione generale. Ma la domanda era ugualmente generica. Fai in modo che l'OP fornisca informazioni specifiche e sarei in grado di dare una risposta più "eloquente". –

+2

1/x può essere calcolato rapidamente .. ma come si fa a far sì che un compilatore emetta RCPSS? – harold

3

Prima di tutto, se si attivano le ottimizzazioni del compilatore, è probabile che il compilatore ottimizzi il calcolo, se possibile (per estrarlo da un ciclo, ad esempio). Per vedere questa ottimizzazione, è necessario creare ed eseguire in modalità di rilascio.

La divisione può essere più pesante della moltiplicazione (ma un commentatore ha sottolineato che i reciproci sono veloci quanto la moltiplicazione sulle CPU moderne, nel qual caso, questo non è corretto per il tuo caso), quindi se hai 1/X da qualche parte all'interno di un ciclo (e più di una volta), è possibile assistere memorizzando nella cache il risultato all'interno del ciclo (float Y = 1.0f/X;) e quindi utilizzando Y. (L'ottimizzazione del compilatore potrebbe farlo in ogni caso.)

Inoltre, alcune formule possono essere ridisegnate per rimuovere la divisione o altri calcoli inefficienti. Per questo, potresti pubblicare il calcolo più grande che viene eseguito. Anche lì, il programma o l'algoritmo stesso può a volte essere ristrutturato per evitare di dover colpire frequentemente i loop che richiedono tempo.

Quanta precisione può essere sacrificata? Se per caso hai solo bisogno di un ordine di grandezza, puoi farlo facilmente usando l'operatore modulo o operazioni bit a bit.

Tuttavia, in generale, non c'è modo di accelerare la divisione. Se ci fossero, i compilatori lo farebbero già.

+0

* Se per caso hai solo bisogno di un ordine di grandezza, puoi farlo facilmente usando l'operatore modulo o operazioni bit a bit. * Come? – klm123

+1

Non intendevo implicare "banale". Inoltre, avrei dovuto aggiungere l'avvertenza che X >> 1 (vedere la fine del commento). In tal caso, puoi sfruttare X^-1 = 2^(- log_2 (X)) e utilizzare http://en.wikipedia.org/wiki/Find_first_set#Algorithms per ottenere l'ordine di grandezza di log_2 (X), per ottenere l'ordine di grandezza nella forma 2^-n. Se i limiti superiore e inferiore su X sono noti, questo potrebbe essere usato per migliorare la velocità. Se le altre grandezze nel calcolo (non mostrate nella domanda) hanno limiti noti e sono alquanto commisurate in ordine di grandezza, possono essere ridimensionate e convertite in interi. –

+1

I compilatori possono solo sollevare Y = 1.0f/X di un ciclo se si utilizza '-ffast-math', quindi è una buona idea farlo nell'origine se non si prevede di abilitare' -ffast-math' per dire al compilatore che non si Non importa dove/quando/come avviene l'arrotondamento o in quale ordine. –

0

Il modo più veloce che conosco è utilizzare le operazioni SIMD. http://msdn.microsoft.com/en-us/library/796k1tty(v=vs.90).aspx

+1

Oppure acquistare una cpu più veloce? :) La domanda è algoritmica. – klm123

+0

O forse approfitta della piena capacità della tua CPU attuale, appena utilizzata? Exploit di branch-forecast? Forse approfittando anche della razionalizzazione? Solo un pensiero ......... –

+2

RCPSS/[RCPPS] (http://www.felixcloutier.com/x86/RCPPS.html) è un buon suggerimento. L'inverso approssimativo veloce (e il sqrt inverso) sono disponibili nell'hardware su x86, per il vettore scalare o SIMD di float. Non è necessario utilizzare SIMD per il resto del ciclo per trarne vantaggio. Se questa risposta avesse spiegato una cosa del genere, non avrebbe ottenuto commenti così confusi. –

4

Credo che quello che cercava sia un modo più efficiente di approssimare 1.0/x invece di una definizione tecnica di approssimazione che afferma che si potrebbe usare 1 come risposta molto impercisa. Credo anche che questo lo soddisfi.

__inline__ double __attribute__((const)) reciprocal(unsigned long long x) { 
    //The type is unsigned long long, but you are restricted to a max value of 2^32-1, not 
    // 2^64-1 like the unsigned long long is capable of storing 
    union { 
     double dbl; 
     unsigned long long ull; 
    } u = {.dbl=(x*=x)};  // x*x = pow(x, 2) 
    u.ull = (0xbfcdd6a18f6a6f52ULL - u.ull) >> (unsigned char)1; 
           // pow(pow(x,2), -0.5) = pow(x, -1) = 1.0/x 
           // This is done via the 'fast' inverse square root trick 
    return u.dbl; 
} 


__inline__ double __attribute__((const)) reciprocal(double x) { 
    union { 
     double dbl; 
     unsigned long long ull; 
    } u; 
    u.dbl = x; 
    u.ull = (0xbfcdd6a18f6a6f52ULL - u.ull) >> (unsigned char)1; 
            // pow(x, -0.5) 
    u.dbl *= u.dbl;     // pow(pow(x,-0.5), 2) = pow(x, -1) = 1.0/x 
    return u.dbl; 
} 


__inline__ float __attribute__((const)) reciprocal(float x) { 
    union { 
     float dbl; 
     unsigned uint; 
    } u; 
    u.dbl = x; 
    u.uint = (0xbe6eb3beU - u.uint) >> (unsigned char)1; 
            // pow(x, -0.5) 
    u.dbl *= u.dbl;     // pow(pow(x,-0.5), 2) = pow(x, -1) = 1.0/x 
    return u.dbl; 
} 


Hmm ....... io feritore se la CPU produce sapeva si potrebbe ottenere il reciproco con un solo moltiplicare , sottrazione e spostamento di bit quando hanno progettato la CPU .... hmm .........

Per quanto riguarda i bench-marking, l'hardware x istruzioni in combinazione con le istruzioni hardware sottrazione sono altrettanto velocemente come hardware 1.0/x istruzioni su moderni computer al giorno (i miei punti di riferimento erano su un i7 di Intel, ma vorrei assumere risultati simili per altri processori). Tuttavia, se questo algoritmo fosse implementato nell'hardware come una nuova istruzione di assemblaggio, allora l'aumento della velocità sarebbe probabilmente abbastanza buono da rendere questa istruzione abbastanza pratica.

Infine, questa implementazione si basa sul meraviglioso "fast" inverse square root algorithm.

+1

Puoi spiegare il numero magico e quale rappresentazione in virgola mobile assume. –

+1

che è molto interessante. Grazie! Avete dei risultati per test comparativi di precisione e velocità? – klm123

+2

Hai provato questo contro l'istruzione approssimativa reciproca di x86, ['RCPSS'] (http://www.felixcloutier.com/x86/RCPSS.html) sul tuo i7? È veloce quanto un intero si moltiplica e non richiede lo spostamento dei dati dai registri XMM all'intero. Puoi usarlo dal C++ con '_mm_rcp_ss (_mm_set_ss (x))'. gcc e clang convertiranno '1.0/x' in RCPSS + un'iterazione di Newton-Raphson, se si utilizza -ffast-math, ma penso che si debba usare manualmente l'intrinseco se si desidera il valore senza un passo di approssimazione. –

0

Questo dovrebbe farlo con un certo numero di pre-srotolati iterazioni di Newton valutato come un polinomio Horner che usa fuso-moltiplicare accumulare operazioni più moderne CPU giornata di eseguire in un unico ciclo Clk (ogni volta):

float inv_fast(float x) { 
    union { float f; int i; } v; 
    float w, sx; 
    int m; 

    sx = (x < 0) ? -1:1; 
    x = sx * x; 

    v.i = (int)(0x7EF127EA - *(uint32_t *)&x); 
    w = x * v.f; 

    // Efficient Iterative Approximation Improvement in horner polynomial form. 
    v.f = v.f * (2 - w);  // Single iteration, Err = -3.36e-3 * 2^(-flr(log2(x))) 
    // v.f = v.f * (4 + w * (-6 + w * (4 - w))); // Second iteration, Err = -1.13e-5 * 2^(-flr(log2(x))) 
    // v.f = v.f * (8 + w * (-28 + w * (56 + w * (-70 + w *(56 + w * (-28 + w * (8 - w))))))); // Third Iteration, Err = +-6.8e-8 * 2^(-flr(log2(x))) 

    return v.f * sx; 
} 

Fine Print: più vicino a 0, l'approssimazione non è ottimale, quindi il programmatore deve testare le prestazioni o limitare l'ingresso al minimo prima di ricorrere alla divisione hardware. sii responsabile!