Il doppio cast a unsigned int su Win32 viene troncato a 2.147.483.648


86

Compilazione del codice seguente:

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

Uscite (MSVC x86):

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

Uscite (MSVC x64):

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

Nella documentazione Microsoft non viene menzionato il valore massimo intero con segno nelle conversioni da doublea unsigned int.

Tutti i valori sopra INT_MAXvengono troncati a 2147483648quando è il ritorno di una funzione.

Sto usando Visual Studio 2019 per creare il programma. Questo non accade su gcc .

Sto facendo qualcosa di sbagliato? C'è un modo sicuro per convertirsi doublein unsigned int?


24
E no, non stai facendo nulla di sbagliato (forse oltre a provare a utilizzare il compilatore "C" di Microsoft)
Antti Haapala

5
Funziona sulla mia macchina ™, testato su VS2017 v15.9.18 e VS2019 v16.4.1. Usa Aiuto> Invia feedback> Segnala un bug per comunicare loro la tua versione.
Hans Passant,

5
Sono in grado di riprodurmi, ho gli stessi risultati di quelli dell'OP. VS2019 16.7.3.
anastaciu

2
@EricPostpischil in effetti, è il bit-pattern diINT_MIN
Antti Haapala il

Risposte:


71

Un bug del compilatore ...

Dall'assembly fornito da @anastaciu, il codice cast diretto chiama __ftol2_sse, che sembra convertire il numero in un long firmato . Il nome della routine è ftol2_sseperché questa è una macchina abilitata per sse, ma il float è in un registro a virgola mobile x87.

; 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

Il cast indiretto invece sì

; 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

che apre e memorizza il valore double nella variabile locale, quindi lo carica in un registro SSE e chiama __dtoui3che è una routine di conversione da double a unsigned int ...

Il comportamento del cast diretto non è conforme a C89; né è conforme a nessuna revisione successiva - anche C89 dice esplicitamente che:

Non è necessario eseguire l'operazione di remaindering eseguita quando un valore di tipo integrale viene convertito in tipo senza segno quando un valore di tipo mobile viene convertito in tipo senza segno. Pertanto l'intervallo di valori portabili è [0, Utype_MAX + 1) .


Credo che il problema potrebbe essere una continuazione di questo dal 2005 - c'era una funzione di conversione chiamata __ftol2che probabilmente avrebbe funzionato per questo codice, cioè avrebbe convertito il valore in un numero con segno -2147483647, che avrebbe prodotto il corretto risultato quando viene interpretato un numero senza segno.

Sfortunatamente __ftol2_ssenon è un sostituto __ftol2immediato di, in quanto, invece di prendere semplicemente i bit di valore meno significativi così come sono, segnalare l'errore fuori intervallo restituendo LONG_MIN/ 0x80000000, che, interpretato come un segno lungo qui non è a tutto quello che ci si aspettava. Il comportamento di __ftol2_ssesarebbe valido per signed long, poiché la conversione di un valore double a> LONG_MAXin signed longavrebbe un comportamento indefinito.


23

Seguendo la risposta di @ AnttiHaapala , ho testato il codice utilizzando l'ottimizzazione /Oxe ho scoperto che questo rimuoverà il bug poiché __ftol2_ssenon viene più utilizzato:

//; 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

Le ottimizzazioni incorporate getdouble()e l'aggiunta di una valutazione costante dell'espressione, eliminando così la necessità di una conversione in fase di esecuzione, eliminando il bug.

Solo per curiosità, ho fatto altri test, vale a dire cambiare il codice per forzare la conversione da float a int in fase di esecuzione. In questo caso il risultato è comunque corretto, il compilatore, con ottimizzazione, utilizza __dtoui3in entrambe le conversioni:

//; 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

Tuttavia, la prevenzione dell'inlining __declspec(noinline) double getDouble(){...}riporterà il bug:

//; 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_sseviene chiamato in entrambe le conversioni rendendo l'output 2147483648in entrambe le situazioni, i sospetti di @zwol erano corretti.


Dettagli della compilazione:

  • Utilizzando la riga di comando:
cl /permissive- /GS /analyze- /W3 /Gm- /Ox /sdl /D "WIN32" program.c        
  • In Visual Studio:

    • Disattivazione RTCin Project -> Properties -> Code Generatione impostando controlli di base di runtime per impostazione predefinita .

    • Abilitazione dell'ottimizzazione in Project -> Properties -> Optimizatione impostazione dell'ottimizzazione su / Ox .

    • Con debugger in x86modalità.


5
Strano come sono come "ok con le ottimizzazioni abilitate, il comportamento non definito sarà davvero indefinito" => il codice funziona effettivamente correttamente: F
Antti Haapala

3
@ AnttiHaapala, sì, sì, Microsoft al suo meglio.
anastaciu

1
Le ottimizzazioni applicate erano inlining e quindi valutazione costante dell'espressione. Non sta più facendo una conversione da float a int in fase di esecuzione. Mi chiedo se il bug ritorni se forzi getDoublefuori linea e / o lo modifichi per restituire un valore che il compilatore non può provare sia costante.
Zwol,

1
@zwol, avevi ragione, forzare il fuori linea e impedire una valutazione costante riporterà il bug, ma questa volta in entrambe le conversioni.
anastaciu

7

Nessuno ha esaminato l'ASM per la SM __ftol2_sse.

Dal risultato, possiamo dedurre che probabilmente è stato convertito da x87 a signed int/ long(entrambi i tipi a 32 bit su Windows), invece che in modo sicuro a uint32_t.

x86 FP -> le istruzioni intere che superano il risultato intero non si limitano a racchiudere / troncare: producono quello che Intel chiama "intero indefinito" quando il valore esatto non è rappresentabile nella destinazione: bit alto impostato, altri bit chiari. cioè0x80000000 .

(Oppure, se l'eccezione FP non valida non è mascherata, si attiva e nessun valore viene memorizzato. Ma nell'ambiente FP predefinito, tutte le eccezioni FP sono mascherate. Ecco perché per i calcoli FP è possibile ottenere un NaN invece di un errore.)

Ciò include sia istruzioni x87 come fistp(utilizzando la modalità di arrotondamento corrente) che istruzioni SSE2 come cvttsd2si eax, xmm0(utilizzando il troncamento verso 0, questo è ciò che tsignifica extra ).

Quindi è un bug da compilare double-> unsignedconversione in una chiamata a __ftol2_sse.


Nota a margine / tangente:

Su x86-64, FP -> uint32_t può essere compilato cvttsd2si rax, xmm0, convertendolo in una destinazione con segno a 64 bit, producendo l'uint32_t che si desidera nella metà inferiore (EAX) della destinazione intera.

È C e C ++ UB se il risultato è al di fuori dell'intervallo 0..2 ^ 32-1 quindi è ok che enormi valori positivi o negativi lasceranno la metà inferiore dello zero RAX (EAX) dal numero intero indefinito di bit-pattern. (A differenza delle conversioni integer-> integer, la riduzione del modulo del valore non è garantita. Il comportamento di lanciare un double negativo a unsigned int è definito nello standard C? Comportamento diverso su ARM rispetto a x86 . Per essere chiari, nulla nella domanda è un comportamento indefinito o anche definito dall'implementazione. Sto solo facendo notare che se hai FP-> int64_t, puoi usarlo per implementare in modo efficiente FP-> uint32_t. Ciò include x87fistp che può scrivere una destinazione intera a 64 bit anche in modalità a 32 e 16 bit, a differenza delle istruzioni SSE2 che possono gestire direttamente solo interi a 64 bit in modalità a 64 bit.


1
Sarei tentato di esaminare quel codice ma fortunatamente non ho MSVC ...: D
Antti Haapala

@ AnttiHaapala: Sì, nemmeno io
Peter Cordes il
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.