Vorzeichenänderungen beim Übergang von int nach float und zurück

Lesezeit: 11 Minuten

Benutzer-Avatar
fredoverflow

Betrachten Sie den folgenden Code, der eine ist SSCCE zu meinem eigentlichen problem:

#include <iostream>

int roundtrip(int x)
{
    return int(float(x));
}

int main()
{
    int a = 2147483583;
    int b = 2147483584;
    std::cout << a << " -> " << roundtrip(a) << '\n';
    std::cout << b << " -> " << roundtrip(b) << '\n';
}

Die Ausgabe auf meinem Computer (Xubuntu 12.04.3 LTS) lautet:

2147483583 -> 2147483520
2147483584 -> -2147483648

Beachten Sie, wie die positive Zahl b endet nach dem Roundtrip negativ. Ist dieses Verhalten gut spezifiziert? Ich hätte erwartet, dass int-to-float-Roundtripping zumindest das Vorzeichen korrekt beibehält …

Hm, auf ideonedie Ausgabe ist anders:

2147483583 -> 2147483520
2147483584 -> 2147483647

Hat das g++-Team in der Zwischenzeit einen Fehler behoben oder sind beide Ausgaben vollkommen gültig?

  • Ich kann das von dir beschriebene Verhalten (nicht das von ideone) bestätigen g++ (GCC) 4.8.2 20131017 (Red Hat 4.8.2-1) auf x86_64.

    – Jonas Schäfer

    8. Dezember 2013 um 12:38 Uhr


  • @Mat: kann das bestätigen, egal welche -O{s,1,2,3} es ist.

    – Jonas Schäfer

    8. Dezember 2013 um 12:40 Uhr

  • Ist die Zahl zu groß für die Mantisse?

    – Fiddle Bits

    8. Dezember 2013 um 12:41 Uhr

  • Für mich scheint dies eine Mischung aus der Tatsache zu sein, dass ganze Zahlen normalerweise nicht durch Gleitkommazahlen dargestellt werden können, UND einem ganzzahligen Überlauf / Umlauf. Versuchen Sie, die temporären Variablen in roundtrip() auszugeben.

    – Amsel

    8. Dezember 2013 um 12:42 Uhr


Benutzer-Avatar
Pascal Cuoq

Ihr Programm ruft aufgrund eines Überlaufs bei der Konvertierung von Gleitkommazahl in Ganzzahl undefiniertes Verhalten auf. Was Sie sehen, ist nur das übliche Symptom auf x86-Prozessoren.

Das float Wert am nächsten 2147483584 ist 231 genau (die Konvertierung von Integer in Gleitkommazahl rundet normalerweise auf den nächsten Wert, was oben sein kann und in diesem Fall oben ist. Um genau zu sein, das Verhalten bei der Konvertierung von Integer in Gleitkommazahl ist implementierungsdefiniert, die meisten Implementierungen definieren Rundung als „entsprechend dem FPU-Rundungsmodus“ und der Standard-Rundungsmodus der FPU ist das Runden auf den nächsten).

Dann, während Sie von dem Float konvertieren, der 2 darstellt31 zu int, kommt es zu einem Überlauf. Dieser Überlauf ist ein undefiniertes Verhalten. Einige Prozessoren lösen eine Ausnahme aus, andere sättigen. Die IA-32-Anweisung cvttsd2si typischerweise von Compilern erzeugt, kommt immer zurück INT_MIN B. bei Überlauf, unabhängig davon, ob der Schwimmer positiv oder negativ ist.

Sie sollten sich nicht auf dieses Verhalten verlassen, selbst wenn Sie wissen, dass Sie auf einen Intel-Prozessor abzielen: Wenn Sie auf x86-64 abzielen, können Compiler für die Konvertierung von Gleitkommazahlen in Ganzzahlen Folgendes ausgeben: Sequenzen von Anweisungen, die das undefinierte Verhalten ausnutzen, um andere Ergebnisse zurückzugeben, als Sie sonst für den Ziel-Integer-Typ erwarten würden.

  • Interessant. Können wir daraus also schließen, dass ideone nicht auf x86 läuft? 🙂

    – fredoverflow

    8. Dezember 2013 um 12:48 Uhr

  • @FredOverflow Zur gleichen Zeit, als Sie Ihren Kommentar geschrieben haben, habe ich den Link hinzugefügt blog.frama-c.com/index.php?post/2013/10/09/… dessen zweite Hälfte, denke ich, dies beantwortet.

    – Pascal Cuoq

    8. Dezember 2013 um 12:49 Uhr


  • Vielen Dank für die ‘x86-Prozessoren, die immer INT_MIN zurückgeben’ – gut zu wissen, wenn Sie ein Programm debuggen.

    Benutzer2249683

    8. Dezember 2013 um 12:55 Uhr

  • Vorbeigehen -fsanitize=float-cast-overflow zu clang wird alle Fälle davon zur Laufzeit abfangen. clang.llvm.org/docs/…

    – Strkat

    9. Dezember 2013 um 5:00 Uhr


  • @strcat Das würde ich nicht annehmen -fsanitize=float-cast-overflow fängt alle solche Überläufe, bis ich es gründlich getestet hatte. Es ist schwierig, einen Compiler, der nicht von Anfang an darauf ausgelegt ist, um eine solide Laufzeitprüfung zu erweitern, da das Frontend und vorhandene Optimierungen stören können. Aber es sollte sicherlich diesen gewöhnlichen fangen.

    – Pascal Cuoq

    9. Dezember 2013 um 7:34 Uhr

Benutzer-Avatar
Artur

Pascals Antwort ist OK – aber es fehlen Details, was dazu führt, dass einige Benutzer sie nicht verstehen 😉 . Wenn Sie daran interessiert sind, wie es auf niedrigerer Ebene aussieht (unter der Annahme, dass der Coprozessor und keine Software Gleitkommaoperationen verarbeitet), lesen Sie weiter.

In 32-Bit-Float (IEEE 754) können Sie alle ganzen Zahlen von innen speichern [-224…224] Angebot. Ganzzahlen außerhalb des Bereichs können auch exakt als Float dargestellt werden, aber nicht alle. Das Problem ist, dass Sie nur 24 signifikante Bits haben können, mit denen Sie in Float spielen können.

So sieht die Konvertierung von int->float normalerweise auf niedriger Ebene aus:

fild dword ptr[your int]
fstp dword ptr[your float]

Es ist nur eine Folge von 2 Coprozessor-Anweisungen. Lädt zuerst 32-Bit-Int auf den Stack des Comprozessors und wandelt es in 80-Bit-Float um.

Softwareentwicklerhandbuch für Intel® 64- und IA-32-Architekturen

(PROGRAMMIERUNG MIT DER X87 FPU):

Wenn Gleitkomma-, Ganzzahl- oder gepackte BCD-Ganzzahlwerte aus dem Speicher in eines der x87-FPU-Datenregister geladen werden, werden die Werte automatisch in das Gleitkommaformat mit doppelter erweiterter Genauigkeit konvertiert (falls sie nicht bereits in diesem Format vorliegen).

Da FPU-Register Floats mit einer Breite von 80 Bit sind, gibt es kein Problem mit fild hier passt 32bit int perfekt in das 64bit-Signifikand des Gleitkommaformats.

So weit, ist es gut.

Der zweite Teil – fstp ist etwas schwierig und kann überraschend sein. Es soll 80-Bit-Fließkomma in 32-Bit-Fließkomma speichern. Obwohl es nur um ganzzahlige Werte geht (in der Frage), kann der Coprozessor tatsächlich “runden”. Ke? Wie runden Sie ganzzahlige Werte, auch wenn sie im Gleitkommaformat gespeichert sind? ;-).

Ich werde es kurz erklären – sehen wir uns zuerst an, welche Rundungsmodi x87 bietet (sie sind die Inkarnation der IEE 754-Rundungsmodi). X87 fpu hat 4 Rundungsmodi, die durch die Bits Nr. 10 und Nr. 11 des Steuerworts der fpu gesteuert werden:

  • 00 – auf die nächste gerade Zahl – Das gerundete Ergebnis kommt dem unendlich genauen Ergebnis am nächsten. Wenn zwei Werte gleich nahe beieinander liegen, ist das Ergebnis der gerade Wert (d. h. derjenige mit dem niederwertigsten Bit von Null). Standard
  • 01 – Richtung -Inf
  • 10 – Richtung +inf
  • 11 – gegen 0 (d. h. abschneiden)

Sie können mit diesem einfachen Code mit Rundungsmodi spielen (obwohl es anders gemacht werden kann – hier wird ein niedriges Niveau angezeigt):

enum ROUNDING_MODE
{
    RM_TO_NEAREST  = 0x00,
    RM_TOWARD_MINF = 0x01,
    RM_TOWARD_PINF = 0x02,
    RM_TOWARD_ZERO = 0x03 // TRUNCATE
};

void set_round_mode(enum ROUNDING_MODE rm)
{
    short csw;
    short tmp = rm;

    _asm
    {
        push ax
        fstcw [csw]
        mov ax, [csw]
        and ax, ~(3<<10)
        shl [tmp], 10
        or ax, tmp
        mov [csw], ax
        fldcw [csw]
        pop ax
    }
}

Ok, schön, aber wie hängt das mit ganzzahligen Werten zusammen? Geduld … um zu verstehen, warum Sie möglicherweise Rundungsmodi für die Umwandlung von int in Float benötigen, überprüfen Sie die offensichtlichste Methode zur Umwandlung von int in Float – Kürzung (nicht Standard) -, die so aussehen kann:

  • Rekordzeichen
  • negieren Sie Ihr int, wenn es kleiner als Null ist
  • Position ganz links finden 1
  • Verschieben Sie int nach rechts/links, sodass die oben gefundene 1 auf Bit #23 positioniert ist
  • Notieren Sie die Anzahl der Verschiebungen während des Prozesses, damit Sie den Exponenten berechnen können

Und der Code, der dieses Verhalten simuliert, könnte folgendermaßen aussehen:

float int2float(int value)
{
    // handles all values from [-2^24...2^24]
    // outside this range only some integers may be represented exactly
    // this method will use truncation 'rounding mode' during conversion

    // we can safely reinterpret it as 0.0
    if (value == 0) return 0.0;

    if (value == (1U<<31)) // ie -2^31
    {
        // -(-2^31) = -2^31 so we'll not be able to handle it below - use const
        value = 0xCF000000;
        return *((float*)&value);
    }

    int sign = 0;

    // handle negative values
    if (value < 0)
    {
        sign = 1U << 31;
        value = -value;
    }

    // although right shift of signed is undefined - all compilers (that I know) do
    // arithmetic shift (copies sign into MSB) is what I prefer here
    // hence using unsigned abs_value_copy for shift
    unsigned int abs_value_copy = value;

    // find leading one
    int bit_num = 31;
    int shift_count = 0;

    for(; bit_num > 0; bit_num--)
    {
        if (abs_value_copy & (1U<<bit_num))
        {
            if (bit_num >= 23)
            {
                // need to shift right
                shift_count = bit_num - 23;
                abs_value_copy >>= shift_count;
            }
            else
            {
                // need to shift left
                shift_count = 23 - bit_num;
                abs_value_copy <<= shift_count;
            }
            break;
        }
    }

    // exponent is biased by 127
    int exp = bit_num + 127;

    // clear leading 1 (bit #23) (it will implicitly be there but not stored)
    int coeff = abs_value_copy & ~(1<<23);

    // move exp to the right place
    exp <<= 23;

    int ret = sign | exp | coeff;

    return *((float*)&ret);
}

Jetzt Beispiel – Trunkierungsmodus konvertiert 2147483583 zu 2147483520.

2147483583 = 01111111_11111111_11111111_10111111

Während der Int-> Float-Konvertierung müssen Sie die 1 ganz links nach Bit # 23 verschieben. Jetzt ist die führende 1 in Bit#30. Um es in Bit Nr. 23 zu platzieren, müssen Sie eine Rechtsverschiebung um 7 Positionen durchführen. Währenddessen verlieren Sie (sie passen nicht in das 32-Bit-Float-Format) 7 lsb-Bits von rechts (Sie kürzen / hacken). Sie sind:

01111111 = 63

Und 63 ist, was die ursprüngliche Zahl verloren hat:

2147483583 -> 2147483520 + 63

Das Abschneiden ist einfach, ist aber nicht unbedingt das, was Sie wollen und/oder ist für alle Fälle das Beste. Betrachten Sie das folgende Beispiel:

67108871 = 00000100_00000000_00000000_00000111

Der obige Wert kann nicht genau durch Float dargestellt werden, aber überprüfen Sie, was das Abschneiden damit macht. Wie zuvor müssen wir die 1 ganz links nach Bit Nr. 23 verschieben. Dies erfordert, dass der Wert genau um 3 Positionen nach rechts verschoben wird, wobei 3 LSB-Bits verloren gehen (ab jetzt schreibe ich Zahlen anders, die zeigen, wo sich das implizite 24.

00000001.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)

Trunkation hackt 3 nachlaufende Bits und lässt uns zurück 67108864 (67108864+7(3 gehackte Bits)) = 67108871 (denken Sie daran, obwohl wir verschieben, kompensieren wir dies mit einer Exponentenmanipulation – hier weggelassen).

Ist das gut genug? Hey 67108872 ist perfekt darstellbar durch 32bit Float und sollte viel besser sein als 67108864 Rechts? RICHTIG und hier möchten Sie vielleicht über das Runden sprechen, wenn Sie int in 32-Bit-Float konvertieren.

Sehen wir uns nun an, wie der Standardmodus „Auf die nächste gerade Zahl runden“ funktioniert und welche Auswirkungen dies im Fall von OP hat. Betrachten Sie dasselbe Beispiel noch einmal.

67108871 = 00000100_00000000_00000000_00000111

Wie wir wissen, benötigen wir 3 Verschiebungen nach rechts, um die 1 ganz links in Bit 23 zu platzieren:

00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)

Das Verfahren zum „Runden auf die nächste gerade Zahl“ beinhaltet das Finden von 2 Zahlen, die den Eingabewert einklammern 67108871 von unten und oben so nah wie möglich. Denken Sie daran, dass wir immer noch innerhalb der FPU mit 80 Bit arbeiten. Obwohl ich zeige, dass einige Bits verschoben werden, befinden sie sich immer noch in der FPU-Registrierung, werden aber während des Rundungsvorgangs beim Speichern des Ausgabewerts entfernt.

00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out)

2 Werte, die eng geklammert sind 00000000_1.[0000000_00000000_00000000] 111 * 2^26 sind:

von oben:

  00000000_1.[0000000_00000000_00000000] 111 * 2^26
                                     +1
= 00000000_1.[0000000_00000000_00000001] * 2^26 = 67108872

und von unten:

  00000000_1.[0000000_00000000_00000000] * 2^26 = 67108864

Offensichtlich 67108872 ist viel näher dran 67108871 als 67108864 daher Konvertierung vom 32-Bit-Int-Wert 67108871 gibt 67108872 (im Modus zum Runden auf die nächste gerade Zahl).

Jetzt die Zahlen von OP (immer noch auf die nächste gerade Zahl gerundet):

 2147483583 = 01111111_11111111_11111111_10111111
= 00000000_1.[1111111_11111111_11111111] 0111111 * 2^30

Klammerwerte:

oben:

  00000000_1.[1111111_111111111_11111111] 0111111 * 2^30
                                      +1
= 00000000_10.[0000000_00000000_00000000] * 2^30
=  00000000_1.[0000000_00000000_00000000] * 2^31 = 2147483648

Unterseite:

00000000_1.[1111111_111111111_11111111] * 2^30 = 2147483520

Denk daran, dass eben Wort in ‘Runden auf die nächste gerade Zahl’ spielt nur eine Rolle, wenn der Eingabewert in der Mitte zwischen den Klammerwerten liegt. Erst dann Wort eben ausschlaggebend und ‘entscheidet’, welcher Klammerwert gewählt werden soll. Im obigen Fall eben spielt keine Rolle und wir müssen einfach den näheren Wert wählen, der ist 2147483520

Der Fall des letzten OP zeigt das Problem, wo eben Wort zählt. :

 2147483584 = 01111111_11111111_11111111_11000000
= 00000000_1.[1111111_11111111_11111111] 1000000 * 2^30

Klammerwerte sind die gleichen wie zuvor:

oben: 00000000_1.[0000000_00000000_00000000] * 2^31 = 2147483648

Unterseite: 00000000_1.[1111111_111111111_11111111] * 2^30 = 2147483520

Es gibt jetzt keinen näheren Wert (2147483648-2147483584=64=2147483584-2147483520), also müssen wir uns darauf verlassen eben und wählen Sie den höchsten (gerade) Wert 2147483648.

Und hier ist das Problem von OP, das Pascal kurz beschrieben hatte. FPU funktioniert nur mit vorzeichenbehafteten Werten und 2147483648 kann nicht als signed int gespeichert werden, da sein Maximalwert 2147483647 ist, daher Probleme.

Einfacher Beweis (ohne Dokumentationszitate), dass FPU nur mit signierten Werten funktioniert, dh. behandelt jeden Wert als signiert, indem Sie Folgendes debuggen:

unsigned int test = (1u << 31);

_asm
{
    fild [test]
}

Obwohl es so aussieht, als ob der Testwert als unsigned behandelt werden sollte, wird er als -2 geladen31 da es keine separaten Anweisungen zum Laden von vorzeichenbehafteten und vorzeichenlosen Werten in die FPU gibt. Ebenso finden Sie keine Anweisungen, mit denen Sie einen unsignierten Wert von FPU in mem speichern können. Alles ist nur ein Bitmuster, das als signiert behandelt wird, unabhängig davon, wie Sie es in Ihrem Programm deklariert haben.

War lang, hoffe aber, dass jemand etwas daraus lernt.

  • Es ist nicht notwendig anzunehmen, dass der Compiler des OP auf den 387 abzielt. Ein moderner Compiler, der auf einen modernen Intel-Befehlssatz abzielt, wird generiert cvttsd2sidie den kleinsten Wert der verwendeten Registergröße (32- oder 64-Bit) bei Überlauf zurückgibt.

    – Pascal Cuoq

    16. Dezember 2013 um 22:10 Uhr

  • @PascalCuoq: Ja, du hast recht. Mein Beitrag war lang genug, um nicht mehr hinzuzufügen.

    – Artur

    16. Dezember 2013 um 22:24 Uhr

  • Schöne ausführliche Erklärung. Leider glaube ich nicht, dass Compiler x87-Anweisungen mehr verwenden.

    – Markieren Sie Lösegeld

    3. Juni um 16:35 Uhr

1018790cookie-checkVorzeichenänderungen beim Übergang von int nach float und zurück

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy