Podwójne rzutowanie na niepodpisany int w systemie Win32 jest obcięte do 2,147,483,648

86

Kompilowanie następującego kodu:

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

Wyjścia (MSVC x86):

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

Wyjścia (MSVC x64):

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

W dokumentacji firmy Microsoft nie ma wzmianki o maksymalnej wartości całkowitej ze znakiem w konwersjach z doublena unsigned int.

Wszystkie powyższe wartości INT_MAXsą obcinane, 2147483648gdy jest to powrót funkcji.

Do tworzenia programu używam Visual Studio 2019 . To się nie dzieje na gcc .

Czy robię coś złego? Czy istnieje bezpieczny sposób konwersji doublena unsigned int?

Matheus Rossi Saciotto
źródło
24
I nie, nie robisz nic złego (być może poza próbą użycia kompilatora „C” Microsoftu)
Antti Haapala
5
Działa na moim komputerze ™, przetestowano na VS2017 w wersji 15.9.18 i VS2019 w wersji 16.4.1. Użyj opcji Pomoc> Prześlij opinię> Zgłoś błąd, aby poinformować ich o swojej wersji.
Hans Passant
5
Jestem w stanie odtworzyć, mam takie same wyniki jak te z OP. VS2019 16.7.3.
anastaciu
2
@EricPostpischil rzeczywiście, jest to wzór bitowyINT_MIN
Antti Haapala
6
Naprawa oczekująca
Antti Haapala

Odpowiedzi:

71

Błąd kompilatora ...

Z zestawu dostarczonego przez @anastaciu, wywołuje bezpośredni kod cast __ftol2_sse, który wydaje się konwertować liczbę na długi ze znakiem . Nazwa procedury jest ftol2_ssetaka, ponieważ jest to maszyna obsługująca sse - ale liczba zmiennoprzecinkowa znajduje się w rejestrze zmiennoprzecinkowym 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

Z drugiej strony, pośrednia obsada tak

; 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

który wyskakuje i przechowuje wartość double w zmiennej lokalnej, a następnie ładuje ją do rejestru SSE i wywołuje __dtoui3procedurę konwersji typu double do unsigned int ...

Zachowanie bezpośredniego rzutu nie jest zgodne z C89; ani nie jest zgodny z żadną późniejszą wersją - nawet C89 wyraźnie mówi, że:

Pozostała operacja wykonywana, gdy wartość typu całkowitego jest konwertowana na typ bez znaku, nie musi być wykonywana, gdy wartość typu zmiennoprzecinkowego jest konwertowana na typ bez znaku. Zatem zakres przenoszonych wartości wynosi [0, Utype_MAX + 1) .


Myślę, że problem może być kontynuacją tego z 2005 roku - kiedyś była funkcja konwersji o nazwie, __ftol2która prawdopodobnie działałaby dla tego kodu, tj. Zamieniłaby wartość na liczbę ze znakiem -2147483647, co dałoby poprawne wynik podczas interpretacji liczby bez znaku.

Niestety __ftol2_ssenie jest to zamiennik typu drop-in __ftol2, ponieważ zamiast po prostu pobierać najmniej znaczące bity wartości w obecnej postaci - sygnalizuje błąd poza zakresem, zwracając LONG_MIN/ 0x80000000, które, interpretowane jako długość bez znaku, tutaj nie jest równe wszystko, czego się spodziewano. Zachowanie z __ftol2_ssebyłoby ważne dla signed long, ponieważ konwersja podwójnej wartości> LONG_MAXna signed longmiałaby niezdefiniowane zachowanie.

Antti Haapala
źródło
23

Podążając za odpowiedzią @ AnttiHaapala , przetestowałem kod przy użyciu optymalizacji /Oxi stwierdziłem, że to usunie błąd, ponieważ __ftol2_ssenie jest już używany:

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

Optymalizacje wprowadziły getdouble()i dodały stałą ocenę wyrażenia, eliminując w ten sposób potrzebę konwersji w czasie wykonywania, dzięki czemu błąd zniknął.

Z ciekawości wykonałem więcej testów, a mianowicie zmieniłem kod, aby wymusić konwersję typu float na int w czasie wykonywania. W tym przypadku wynik jest nadal poprawny, kompilator z optymalizacją używa __dtoui3w obu konwersjach:

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

Jednak zapobieganie inliningowi __declspec(noinline) double getDouble(){...}przywróci błąd:

//; 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_ssejest wywoływana w obu konwersjach, dając wynik 2147483648w obu sytuacjach, podejrzenia @zwol były prawidłowe.


Szczegóły kompilacji:

  • Korzystanie z wiersza poleceń:
cl /permissive- /GS /analyze- /W3 /Gm- /Ox /sdl /D "WIN32" program.c        
  • W programie Visual Studio:

    • Wyłączenie RTCw Project -> Properties -> Code Generationi ustawienie Sprawdza Podstawowe Runtime aby domyślnie .

    • Włączanie optymalizacji Project -> Properties -> Optimizationi ustawienie Optymalizacji na / Ox .

    • Z debugerem w x86trybie.

anastaciu
źródło
5
Zabawne, jak mówią "ok z włączoną optymalizacją, niezdefiniowane zachowanie będzie naprawdę niezdefiniowane" => kod faktycznie działa poprawnie: F
Antti Haapala
3
@AnttiHaapala, tak, tak, Microsoft w najlepszym wydaniu.
anastaciu
1
Zastosowane optymalizacje polegały na wbudowaniu, a następnie stałej ocenie wyrażenia. To nie jest już konwersja typu float na int w czasie wykonywania. Zastanawiam się, czy błąd powróci, jeśli wymusisz getDoublewyjście z linii i / lub zmienisz ją, aby zwrócić wartość, której kompilator nie może udowodnić, jest stała.
zwol
1
@zwol, miałeś rację, wymuszenie wyjścia z linii i uniemożliwienie ciągłej oceny przywróci błąd, ale tym razem w obu konwersjach.
anastaciu
7

Nikt nie spojrzał na asm dla SM __ftol2_sse.

Z wyniku możemy wywnioskować, że prawdopodobnie został on przekonwertowany z x87 na signed int/ long(oba typy 32-bitowe w systemie Windows), zamiast bezpiecznie na uint32_t.

x86 FP -> instrukcje całkowitoliczbowe, które przepełniają wynik będący liczbą całkowitą, nie tylko zawijają / obcinają: wytwarzają to, co Intel nazywa „liczbą całkowitą nieokreśloną”, gdy dokładna wartość nie jest reprezentowalna w miejscu docelowym: ustawiony wysoki bit, pozostałe bity wyczyszczone. tj0x80000000 .

(Lub jeśli wyjątek niepoprawnego FP nie jest maskowany, jest uruchamiany i żadna wartość nie jest przechowywana. Ale w domyślnym środowisku FP wszystkie wyjątki FP są maskowane. Dlatego w obliczeniach FP można uzyskać NaN zamiast błędu).

Obejmuje to zarówno instrukcje x87, takie jak fistp(przy użyciu bieżącego trybu zaokrąglania), jak i instrukcje SSE2, takie jak cvttsd2si eax, xmm0(przy użyciu obcinania w kierunku 0, to toznacza dodatkowe ).

Więc kompilacja jest błędem double-> unsignedkonwersja na wywołanie __ftol2_sse.


Uwaga boczna / styczna:

Na x86-64, FP -> uint32_t można skompilować do cvttsd2si rax, xmm0, konwertując do 64-bitowego podpisanego miejsca docelowego, tworząc uint32_t, który chcesz w dolnej połowie (EAX) miejsca docelowego będącego liczbą całkowitą.

Jest to C i C ++ UB, jeśli wynik jest poza zakresem 0..2 ^ 32-1, więc jest w porządku, że duże dodatnie lub ujemne wartości pozostawiają dolną połowę RAX (EAX) zero z nieokreślonego wzorca bitowego liczby całkowitej. (W przeciwieństwie do konwersji typu integer-> integer, redukcja modulo wartości nie jest gwarantowana. Czy zachowanie rzutowania ujemnej liczby podwójnej na wartość int bez znaku jest zdefiniowane w standardzie C? Inne zachowanie na ARM w porównaniu z x86 . Żeby było jasne, nic w pytaniu jest niezdefiniowanym lub nawet zdefiniowanym przez implementację zachowaniem. Zwracam tylko uwagę, że jeśli masz FP-> int64_t, możesz go użyć do wydajnej implementacji FP-> uint32_t. Obejmuje to x87fistp który może zapisywać 64-bitowe liczby całkowite nawet w trybie 32-bitowym i 16-bitowym, w przeciwieństwie do instrukcji SSE2, które mogą bezpośrednio obsługiwać tylko 64-bitowe liczby całkowite w trybie 64-bitowym.

Peter Cordes
źródło
1
Kusiłbym, żeby zajrzeć do tego kodu, ale na szczęście nie mam MSVC ...: D
Antti Haapala
@AnttiHaapala: Tak, ja też
Peter Cordes,