Eine schnelle Methode zum Runden eines Double auf ein 32-Bit-Int erklärt

169

Beim Lesen von Luas Quellcode habe ich festgestellt, dass Lua a verwendet macro, um a doubleauf 32 Bit zu runden int. Ich habe das extrahiert macround es sieht so aus:

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

Hier ENDIANLOCwird Endianness definiert , 0für Little Endian, 1für Big Endian. Lua geht vorsichtig mit Endianness um. tsteht für den Integer-Typ, wie intoder unsigned int.

Ich habe ein wenig recherchiert und es gibt ein einfacheres Format macro, das denselben Gedanken verwendet:

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

Oder im C ++ - Stil:

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

Dieser Trick kann auf jeder Maschine mit IEEE 754 funktionieren (was heutzutage so ziemlich jede Maschine bedeutet). Es funktioniert sowohl für positive als auch für negative Zahlen, und die Rundung folgt der Banker-Regel . (Dies ist nicht überraschend, da es IEEE 754 folgt.)

Ich habe ein kleines Programm geschrieben, um es zu testen:

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

Und es gibt erwartungsgemäß -12345679 aus.

Ich möchte ins Detail gehen, wie dies schwierig macrofunktioniert. Die magische Zahl 6755399441055744.0ist tatsächlich 2^51 + 2^52oder 1.5 * 2^52und und 1.5kann binär dargestellt werden als 1.1. Wenn eine 32-Bit-Ganzzahl zu dieser magischen Zahl hinzugefügt wird, bin ich von hier aus verloren. Wie funktioniert dieser Trick?

PS: Dies ist im Lua-Quellcode Llimits.h .

UPDATE :

  1. Wie @Mysticial hervorhebt, beschränkt sich diese Methode nicht auf 32-Bit int, sondern kann auch auf 64-Bit erweitert werden, intsolange die Zahl im Bereich von 2 ^ 52 liegt. (Das macromuss geändert werden.)
  2. Einige Materialien sagen, dass diese Methode in Direct3D nicht verwendet werden kann .
  3. Wenn Sie mit Microsoft Assembler für x86 arbeiten, wird noch schneller macrogeschrieben assembly(dies wird auch aus der Lua-Quelle extrahiert):

    #define double2int(i,n)  __asm {__asm fld n   __asm fistp i}
  4. Es gibt eine ähnliche magische Zahl für Zahlen mit einfacher Genauigkeit: 1.5 * 2 ^23

Yu Hao
quelle
3
"schnell" im Vergleich zu was?
Cory Nelson
3
@CoryNelson Schnell im Vergleich zu einer einfachen Besetzung. Diese Methode ist bei ordnungsgemäßer Implementierung (mit SSE-Intrinsics) buchstäblich hundertmal schneller als eine Besetzung. (was einen fiesen Funktionsaufruf für einen ziemlich teuren Konvertierungscode
hervorruft
2
Richtig - ich kann sehen, dass es schneller ist als ftoi. Aber wenn Sie über SSE sprechen, warum nicht einfach die einzelne Anweisung verwenden CVTTSD2SI?
Cory Nelson
3
@tmyklebu Viele der Anwendungsfälle, die gehen, double -> int64liegen tatsächlich im 2^52Bereich. Diese treten besonders häufig auf, wenn ganzzahlige Faltungen mit Gleitkomma-FFTs durchgeführt werden.
Mysticial
7
@ MSalters Nicht unbedingt wahr. Eine Besetzung muss den Spezifikationen der Sprache entsprechen - einschließlich der ordnungsgemäßen Behandlung von Überlauf- und NAN-Fällen. (oder was auch immer der Compiler im Fall IB oder UB angibt) Diese Überprüfungen sind in der Regel sehr teuer. Der in dieser Frage erwähnte Trick ignoriert solche Eckfälle vollständig. Wenn Sie also die Geschwindigkeit wollen und Ihre Anwendung solche Eckfälle nicht interessiert (oder nie trifft), dann ist dieser Hack perfekt geeignet.
Mysticial

Antworten:

161

A doublewird folgendermaßen dargestellt:

doppelte Darstellung

und es kann als zwei 32-Bit-Ganzzahlen gesehen werden; Jetzt ist die intin allen Versionen Ihres Codes aufgenommene (vorausgesetzt, es handelt sich um eine 32-Bit-Version int) die rechts in der Abbildung. Am Ende nehmen Sie also nur die niedrigsten 32 Bit der Mantisse.


Nun zur magischen Zahl; Wie Sie richtig angegeben haben, ist 6755399441055744 2 ^ 51 + 2 ^ 52; Das Hinzufügen einer solchen Zahl zwingt die doubledazu, in den "süßen Bereich" zwischen 2 ^ 52 und 2 ^ 53 zu gehen, der, wie von Wikipedia hier erklärt , eine interessante Eigenschaft hat:

Zwischen 2 52 = 4,503,599,627,370,496 und 2 53 = 9,007,199,254,740,992 sind die darstellbaren Zahlen genau die ganzen Zahlen

Dies folgt aus der Tatsache, dass die Mantisse 52 Bit breit ist.

Die andere interessante Tatsache beim Hinzufügen von 2 51 +2 52 ist, dass es die Mantisse nur in den zwei höchsten Bits beeinflusst - die sowieso verworfen werden, da wir nur die niedrigsten 32 Bits nehmen.


Zu guter Letzt: das Schild.

IEEE 754-Gleitkomma verwendet eine Größen- und Vorzeichendarstellung, während Ganzzahlen auf "normalen" Maschinen die Komplementarithmetik von 2 verwenden. Wie wird das hier gehandhabt?

Wir haben nur über positive ganze Zahlen gesprochen; Nehmen wir nun an, wir haben es mit einer negativen Zahl in dem Bereich zu tun, der durch ein 32-Bit dargestellt werden kann int, also weniger (im absoluten Wert) als (-2 ^ 31 + 1); nenn es -a. Eine solche Zahl wird offensichtlich durch Addition der magischen Zahl positiv gemacht, und der resultierende Wert ist 2 52 +2 51 + (- a).

Was bekommen wir nun, wenn wir die Mantisse in der Komplementdarstellung von 2 interpretieren? Es muss das Ergebnis der 2er-Komplementsumme von (2 52 +2 51 ) und (-a) sein. Wiederum wirkt sich der erste Term nur auf die oberen zwei Bits aus. Was in den Bits 0 bis 50 verbleibt, ist die Zweierkomplementdarstellung von (-a) (wiederum abzüglich der oberen zwei Bits).

Da die Reduzierung der Komplementzahl einer 2 auf eine kleinere Breite nur durch Wegschneiden der zusätzlichen Bits auf der linken Seite erfolgt, ergibt die korrekte (-a) 32-Komplement-Arithmetik von 2, wenn die unteren 32 Bits verwendet werden.

Matteo Italia
quelle
"" Die andere interessante Tatsache beim Hinzufügen von 2 ^ 51 + 2 ^ 52 ist, dass es die Mantisse nur in den zwei höchsten Bits beeinflusst - die sowieso verworfen werden, da wir nur die niedrigsten 32 Bits nehmen "" "Was ist das? Wenn Sie dies hinzufügen, kann sich die gesamte Mantisse verschieben!
YvesgereY
@John: Natürlich besteht der Sinn des Hinzufügens darin, den Wert in diesen Bereich zu zwingen, was offensichtlich dazu führen kann, dass die Mantisse (zwischen den anderen Dingen) in Bezug auf den ursprünglichen Wert verschoben wird. Was ich hier gesagt habe ist, dass, sobald Sie sich in diesem Bereich befinden, die einzigen Bits, die sich von der entsprechenden 53-Bit-Ganzzahl unterscheiden, die Bits 51 und 52 sind, die ohnehin verworfen werden.
Matteo Italia
2
Für diejenigen, die zu int64_tIhnen konvertieren möchten, können Sie dies tun, indem Sie die Mantisse um 13 Bit nach links und dann nach rechts verschieben. Dadurch werden der Exponent und die beiden Bits von der 'magischen' Zahl entfernt, das Vorzeichen wird jedoch beibehalten und an die gesamte 64-Bit-Ganzzahl mit Vorzeichen weitergegeben. union { double d; int64_t l; } magic; magic.d = input + 6755399441055744.0; magic.l <<= 13; magic.l >>= 13;
Wojciech Migda