Die doppelte Umwandlung in Int ohne Vorzeichen unter Win32 wird auf 2.147.483.648 abgeschnitten

86

Kompilieren des folgenden Codes:

double getDouble()
{
    double value = 2147483649.0;
    return value;
}

int main()
{
     printf("INT_MAX: %u\n", INT_MAX);
     printf("UINT_MAX: %u\n", UINT_MAX);

     printf("Double value: %f\n", getDouble());
     printf("Direct cast value: %u\n", (unsigned int) getDouble());
     double d = getDouble();
     printf("Indirect cast value: %u\n", (unsigned int) d);

     return 0;
}

Ausgänge (MSVC x86):

INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483648
Indirect cast value: 2147483649

Ausgänge (MSVC x64):

INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483649
Indirect cast value: 2147483649

In der Microsoft-Dokumentation wird der vorzeichenbehaftete ganzzahlige Maximalwert bei Konvertierungen von doublebis nicht erwähnt unsigned int.

Alle oben genannten Werte INT_MAXwerden abgeschnitten, 2147483648wenn eine Funktion zurückgegeben wird.

Ich verwende Visual Studio 2019 , um das Programm zu erstellen. Dies passiert auf gcc nicht .

Mache ich etwas falsch Gibt es einen sicheren Weg, um doublezu konvertieren unsigned int?

Matheus Rossi Saciotto
quelle
24
Und nein, Sie machen nichts falsch (vielleicht außer dem Versuch, den "C" -Compiler von Microsoft zu verwenden)
Antti Haapala
5
Funktioniert auf meinem Computer ™, getestet auf VS2017 v15.9.18 und VS2019 v16.4.1. Verwenden Sie Hilfe> Feedback senden> Fehler melden, um sie über Ihre Version zu informieren.
Hans Passant
5
Ich kann reproduzieren, ich habe die gleichen Ergebnisse wie die des OP. VS2019 16.7.3.
Anastaciu
2
@ EricPostpischil in der Tat, es ist das Bitmuster vonINT_MIN
Antti Haapala
6
Fix ausstehend
Antti Haapala

Antworten:

71

Ein Compiler-Fehler ...

Aus der von @anastaciu bereitgestellten Assembly wird der direkte Cast-Code aufgerufen, der __ftol2_ssedie Nummer in eine signierte Long- Nummer umzuwandeln scheint . Der Routinenname ist, ftol2_sseweil dies eine sse-fähige Maschine ist - aber der Float befindet sich in einem x87-Gleitkommaregister.

; Line 17
    call    _getDouble
    call    __ftol2_sse
    push    eax
    push    OFFSET ??_C@_0BH@GDLBDFEH@Direct?5cast?5value?3?5?$CFu?6@
    call    _printf
    add esp, 8

Die indirekte Besetzung dagegen tut es

; Line 18
    call    _getDouble
    fstp    QWORD PTR _d$[ebp]
; Line 19
    movsd   xmm0, QWORD PTR _d$[ebp]
    call    __dtoui3
    push    eax
    push    OFFSET ??_C@_0BJ@HCKMOBHF@Indirect?5cast?5value?3?5?$CFu?6@
    call    _printf
    add esp, 8

Dadurch wird der doppelte Wert in die lokale Variable eingefügt und dort gespeichert. Anschließend wird er in ein SSE-Register geladen und __dtoui3aufgerufen. Dies ist eine Konvertierungsroutine mit doppeltem zu vorzeichenlosem int ...

Das Verhalten der Direktbesetzung entspricht nicht C89; Es entspricht auch keiner späteren Überarbeitung - selbst C89 sagt ausdrücklich Folgendes:

Die verbleibende Operation, die ausgeführt wird, wenn ein Wert vom Typ Integral in einen Typ ohne Vorzeichen konvertiert wird, muss nicht ausgeführt werden, wenn ein Wert vom Typ Floating in einen Typ ohne Vorzeichen konvertiert wird. Somit ist der Bereich der tragbaren Werte [0, Utype_MAX + 1) .


Ich glaube, das Problem könnte eine Fortsetzung davon aus dem Jahr 2005 sein - früher gab es eine Konvertierungsfunktion, __ftol2die wahrscheinlich für diesen Code funktioniert hätte, dh sie hätte den Wert in eine signierte Nummer -2147483647 konvertiert, die die korrekte erzeugt hätte Ergebnis bei Interpretation einer vorzeichenlosen Zahl.

Leider __ftol2_sseist dies kein Drop-In-Ersatz für __ftol2, da es - anstatt nur die niedrigstwertigen Wertbits unverändert zu verwenden - den Fehler außerhalb des Bereichs signalisiert, indem es LONG_MIN/ zurückgibt 0x80000000, das, hier als vorzeichenlos lang interpretiert, nicht bei ist alles was erwartet wurde. Das Verhalten von __ftol2_ssewäre gültig für signed long, da die Konvertierung eines doppelten Wertes> LONG_MAXin signed longein undefiniertes Verhalten hätte.

Antti Haapala
quelle
23

Nach der Antwort von @ AnttiHaapala habe ich den Code mithilfe der Optimierung getestet /Oxund festgestellt, dass dadurch der Fehler behoben wird, der __ftol2_ssenicht mehr verwendet wird:

//; 17   :     printf("Direct cast value: %u\n", (unsigned int)getDouble());

    push    -2147483647             //; 80000001H
    push    OFFSET $SG10116
    call    _printf

//; 18   :     double d = getDouble();
//; 19   :     printf("Indirect cast value: %u\n", (unsigned int)d);

    push    -2147483647             //; 80000001H
    push    OFFSET $SG10117
    call    _printf
    add esp, 28                 //; 0000001cH

Durch die Optimierungen wurde getdouble()eine konstante Ausdrucksbewertung hinzugefügt und hinzugefügt, sodass zur Laufzeit keine Konvertierung erforderlich ist und der Fehler behoben wird .

Aus Neugier habe ich einige weitere Tests durchgeführt, nämlich das Ändern des Codes, um die Umwandlung von Float in Int zur Laufzeit zu erzwingen. In diesem Fall ist das Ergebnis immer noch korrekt. Der Compiler verwendet bei der Optimierung __dtoui3beide Konvertierungen:

//; 19   :     printf("Direct cast value: %u\n", (unsigned int)getDouble(d));

    movsd   xmm0, QWORD PTR _d$[esp+24]
    add esp, 12                 //; 0000000cH
    call    __dtoui3
    push    eax
    push    OFFSET $SG9261
    call    _printf

//; 20   :     double db = getDouble(d);
//; 21   :     printf("Indirect cast value: %u\n", (unsigned int)db);

    movsd   xmm0, QWORD PTR _d$[esp+20]
    add esp, 8
    call    __dtoui3
    push    eax
    push    OFFSET $SG9262
    call    _printf

Wenn Sie jedoch Inlining verhindern, __declspec(noinline) double getDouble(){...}wird der Fehler zurückgebracht:

//; 17   :     printf("Direct cast value: %u\n", (unsigned int)getDouble(d));

    movsd   xmm0, QWORD PTR _d$[esp+76]
    add esp, 4
    movsd   QWORD PTR [esp], xmm0
    call    _getDouble
    call    __ftol2_sse
    push    eax
    push    OFFSET $SG9261
    call    _printf

//; 18   :     double db = getDouble(d);

    movsd   xmm0, QWORD PTR _d$[esp+80]
    add esp, 8
    movsd   QWORD PTR [esp], xmm0
    call    _getDouble

//; 19   :     printf("Indirect cast value: %u\n", (unsigned int)db);

    call    __ftol2_sse
    push    eax
    push    OFFSET $SG9262
    call    _printf

__ftol2_ssewird in beiden Konvertierungen aufgerufen, wodurch die Ausgabe 2147483648in beiden Situationen erfolgt, @zwol-Verdächtigungen waren korrekt.


Zusammenstellungsdetails:

  • Verwenden der Befehlszeile:
cl /permissive- /GS /analyze- /W3 /Gm- /Ox /sdl /D "WIN32" program.c        
  • In Visual Studio:

    • Die Deaktivierung RTCin Project -> Properties -> Code Generationund Einstellung Basic Runtime Checks auf Standard .

    • Aktivieren der Optimierung in Project -> Properties -> Optimizationund Setzen der Optimierung auf / Ox .

    • Mit Debugger im x86Modus.

anastaciu
quelle
5
Komisch, wie sie lauten: "OK, wenn Optimierungen aktiviert sind, wird undefiniertes Verhalten wirklich undefiniert sein" => Der Code funktioniert tatsächlich korrekt: F
Antti Haapala
3
@AnttiHaapala, ja, ja, Microsoft vom Feinsten.
Anastaciu
1
Die angewendeten Optimierungen waren Inlining und dann Bewertung der konstanten Expression. Zur Laufzeit wird keine Float-to-Int-Konvertierung mehr durchgeführt. Ich frage mich, ob der Fehler erneut auftritt, wenn Sie eine Abweichung erzwingen getDoubleund / oder ihn ändern, um einen Wert zurückzugeben, den der Compiler nicht als konstant beweisen kann.
Zwol
1
@zwol, Sie hatten Recht, wenn Sie eine Abweichung erzwingen und eine ständige Auswertung verhindern, wird der Fehler zurückgebracht, diesmal jedoch in beiden Konvertierungen.
Anastaciu
7

Niemand hat sich den Asm für MS angesehen __ftol2_sse.

Aus dem Ergebnis können wir schließen, dass es wahrscheinlich von x87 in signiertes int/ konvertiert wurde long(beide 32-Bit-Typen unter Windows), anstatt sicher in uint32_t.

x86 FP -> Integer-Anweisungen, die das Integer-Ergebnis überlaufen, werden nicht nur umgebrochen / abgeschnitten: Sie erzeugen das, was Intel als "Integer Indefinite" bezeichnet, wenn der genaue Wert im Ziel nicht darstellbar ist: High-Bit gesetzt, andere Bits gelöscht. dh0x80000000 .

(Wenn die ungültige FP-Ausnahme nicht maskiert ist, wird sie ausgelöst und es wird kein Wert gespeichert. In der Standard-FP-Umgebung werden jedoch alle FP-Ausnahmen maskiert. Aus diesem Grund können Sie für FP-Berechnungen anstelle eines Fehlers eine NaN erhalten.)

Dies beinhaltet sowohl x87-Anweisungen wie fistp(unter Verwendung des aktuellen Rundungsmodus) als auch SSE2-Anweisungen wie cvttsd2si eax, xmm0(unter Verwendung der Kürzung in Richtung 0, das tbedeutet das Extra ).

Es ist also ein Fehler beim Kompilieren double-> unsignedKonvertieren in einen Aufruf von __ftol2_sse.


Randnotiz / Tangente:

Auf x86-64 kann FP -> uint32_t kompiliert werden cvttsd2si rax, xmm0, um in ein 64-Bit-Ziel mit Vorzeichen konvertiert zu werden, und das gewünschte uint32_t in der unteren Hälfte (EAX) des ganzzahligen Ziels erzeugen.

Es ist C und C ++ UB, wenn das Ergebnis außerhalb des Bereichs 0..2 ^ 32-1 liegt. Es ist also in Ordnung, dass große positive oder negative Werte die niedrige Hälfte von RAX (EAX) Null aus dem ganzzahligen unbestimmten Bitmuster verlassen. (Im Gegensatz zu Integer-> integer Conversions Moduloreduktion des Wertes ist nicht garantiert. Ist das Verhalten einer negativen Doppel in unsigned int Gießen definiert in der C - Standard? Unterschiedliches Verhalten auf ARM vs. x86 . Um es klar, nichts in der Frage ist undefiniertes oder sogar implementierungsdefiniertes Verhalten. Ich möchte nur darauf hinweisen, dass Sie FP-> int64_t verwenden können, um FP-> uint32_t effizient zu implementieren. Dazu gehört x87fistp Im Gegensatz zu SSE2-Anweisungen, die 64-Bit-Ganzzahlen im 64-Bit-Modus nur direkt verarbeiten können, kann ein 64-Bit-Ganzzahlziel auch im 32-Bit- und 16-Bit-Modus geschrieben werden.

Peter Cordes
quelle
1
Ich wäre versucht, diesen Code zu untersuchen, aber zum Glück habe ich keine MSVC ...: D
Antti Haapala
@AnttiHaapala: Ja, ich auch nicht
Peter Cordes