Spiegazione di un metodo rapido per arrotondare un doppio a un int a 32 bit


169

Durante la lettura del codice sorgente di Lua , ho notato che Lua usa a macroper arrotondare doublea a 32 bit int. Ho estratto il macro, e si presenta così:

union i_cast {double d; int i[2]};
#define double2int(i, d, t)  \
    {volatile union i_cast u; u.d = (d) + 6755399441055744.0; \
    (i) = (t)u.i[ENDIANLOC];}

Qui ENDIANLOCè definito come endianness , 0per little endian, 1per big endian. Lua gestisce con cura l'endianità. tsta per il tipo intero, come into unsigned int.

Ho fatto una piccola ricerca e c'è un formato più semplice macroche usa lo stesso pensiero:

#define double2int(i, d) \
    {double t = ((d) + 6755399441055744.0); i = *((int *)(&t));}

O in stile C ++:

inline int double2int(double d)
{
    d += 6755399441055744.0;
    return reinterpret_cast<int&>(d);
}

Questo trucco può funzionare su qualsiasi macchina usando IEEE 754 (il che significa praticamente ogni macchina oggi). Funziona sia per i numeri positivi che per quelli negativi e l'arrotondamento segue la regola del banchiere . (Questo non è sorprendente, dal momento che segue IEEE 754.)

Ho scritto un piccolo programma per testarlo:

int main()
{
    double d = -12345678.9;
    int i;
    double2int(i, d)
    printf("%d\n", i);
    return 0;
}

E genera -12345679, come previsto.

Vorrei entrare nel dettaglio di come funziona questo trucco macro. Il numero magico 6755399441055744.0è in realtà 2^51 + 2^52, oppure 1.5 * 2^52, e 1.5in binario può essere rappresentato come 1.1. Quando un numero intero a 32 bit viene aggiunto a questo numero magico, beh, mi perdo da qui. Come funziona questo trucco?

PS: Questo è nel codice sorgente di Lua, Llimits.h .

AGGIORNAMENTO :

  1. Come sottolinea @Mysticial, questo metodo non si limita a 32 bit int, ma può anche essere espanso a 64 bit intpurché il numero sia compreso nell'intervallo 2 ^ 52. ( macroRichiede alcune modifiche.)
  2. Alcuni materiali affermano che questo metodo non può essere utilizzato in Direct3D .
  3. Quando si lavora con l'assemblatore Microsoft per x86, c'è una macroscrittura ancora più veloce assembly(anch'essa estratta dal sorgente Lua):

    #define double2int(i,n)  __asm {__asm fld n   __asm fistp i}
  4. Esiste un numero magico simile per un singolo numero di precisione: 1.5 * 2 ^23


3
"veloce" rispetto a cosa?
Cory Nelson,

3
@CoryNelson Fast rispetto a un cast semplice. Questo metodo, se implementato correttamente (con intrinseci SSE) è letteralmente cento volte più veloce di un cast. (che invoca una brutta chiamata di funzione a un codice di conversione piuttosto costoso)
Mistico

2
Giusto - Vedo che è più veloce di ftoi. Ma se stai parlando SSE, perché non usare semplicemente le singole istruzioni CVTTSD2SI?
Cory Nelson,

3
@tmyklebu Molti dei casi d'uso che vanno double -> int64sono davvero all'interno della 2^52gamma. Questi sono particolarmente comuni quando si eseguono convoluzioni di numeri interi usando FFT a virgola mobile.
Mistico l'

7
@MSalters Non necessariamente vero. Un cast deve essere all'altezza delle specifiche del linguaggio, inclusa la corretta gestione dei casi di overflow e NAN. (o qualunque cosa il compilatore specifichi nel caso IB o UB) Questi controlli tendono ad essere molto costosi. Il trucco menzionato in questa domanda ignora completamente questi casi angolari. Quindi, se vuoi la velocità e la tua applicazione non si preoccupa (o non incontra mai) casi angolari, allora questo hack è perfettamente appropriato.
Mistico

Risposte:


161

A doubleè rappresentato in questo modo:

doppia rappresentazione

e può essere visto come due numeri interi a 32 bit; ora, il intpreso in tutte le versioni del tuo codice (supponendo che sia un 32-bit int) è quello a destra nella figura, quindi quello che stai facendo alla fine è solo prendere i 32 bit più bassi di mantissa.


Ora, al numero magico; come hai affermato correttamente, 6755399441055744 è 2 ^ 51 + 2 ^ 52; l'aggiunta di un tale numero costringe doublead andare nella "gamma dolce" tra 2 ^ 52 e 2 ^ 53, che, come spiegato qui da Wikipedia , ha una proprietà interessante:

Tra 2 52 = 4.503.599.627.370.496 e 2 53 = 9.007.199.254.740.992 i numeri rappresentabili sono esattamente i numeri interi

Ciò deriva dal fatto che la mantissa è larga 52 bit.

L'altro fatto interessante sull'aggiunta di 2 51 +2 52 è che influenza la mantissa solo nei due bit più alti - che vengono comunque scartati, poiché stiamo prendendo solo i suoi 32 bit più bassi.


Ultimo ma non meno importante: il segno.

IEEE 754 in virgola mobile utilizza una rappresentazione di grandezza e segno, mentre numeri interi su macchine "normali" usano l'aritmetica del complemento di 2; come viene gestito qui?

Abbiamo parlato solo di numeri interi positivi; ora supponiamo di avere a che fare con un numero negativo nell'intervallo rappresentabile da un 32 bit int, quindi inferiore (in valore assoluto) di (-2 ^ 31 + 1); chiamalo -a. Un tale numero è ovviamente reso positivo aggiungendo il numero magico e il valore risultante è 2 52 +2 51 + (- a).

Ora, cosa otteniamo se interpretiamo la mantissa nella rappresentazione del complemento di 2? Deve essere il risultato della somma del complemento di 2 di (2 52 +2 51 ) e (-a). Ancora una volta, il primo termine influenza solo i due bit superiori, ciò che rimane nei bit 0 ~ 50 è la rappresentazione del complemento di 2 di (-a) (di nuovo, meno i due bit superiori).

Poiché la riduzione del numero del complemento di un 2 a una larghezza inferiore viene effettuata semplicemente tagliando via i bit extra a sinistra, prendere i 32 bit inferiori ci dà correttamente (-a) in 32 bit, l'aritmetica del complemento di 2.


"" "L'altro fatto interessante sull'aggiunta di 2 ^ 51 + 2 ^ 52 è che influenza la mantissa solo nei due bit più alti - che vengono comunque scartati, poiché stiamo prendendo solo i suoi 32 bit più bassi" "" Che cos'è? L'aggiunta di questo può spostare tutta la mantissa!
YvesgereY,

@Giovanni: ovviamente, il punto centrale di aggiungerli è forzare il valore in quell'intervallo, il che ovviamente può portare a spostare la mantissa (tra le altre cose) rispetto al valore originale. Quello che stavo dicendo qui è che, una volta che ci si trova in quell'intervallo, gli unici bit che differiscono dal corrispondente intero di 53 bit sono i bit 51 e 52, che vengono comunque scartati.
Matteo Italia,

2
Per coloro che desiderano convertirsi a int64_tte, puoi farlo spostando la mantissa a sinistra e poi a destra di 13 bit. Questo cancellerà l'esponente e i due bit dal numero "magico", ma manterrà e propagherà il segno all'intero con segno a 64 bit. union { double d; int64_t l; } magic; magic.d = input + 6755399441055744.0; magic.l <<= 13; magic.l >>= 13;
Wojciech Migda,
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.