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 double
na unsigned int
.
Wszystkie powyższe wartości INT_MAX
są obcinane, 2147483648
gdy 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 double
na unsigned int
?
c
visual-c++
casting
x86
floating-point
Matheus Rossi Saciotto
źródło
źródło
INT_MIN
Odpowiedzi:
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 jestftol2_sse
taka, 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
__dtoui3
procedurę 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:
Myślę, że problem może być kontynuacją tego z 2005 roku - kiedyś była funkcja konwersji o nazwie,
__ftol2
któ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_sse
nie 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ącLONG_MIN
/0x80000000
, które, interpretowane jako długość bez znaku, tutaj nie jest równe wszystko, czego się spodziewano. Zachowanie z__ftol2_sse
byłoby ważne dlasigned long
, ponieważ konwersja podwójnej wartości>LONG_MAX
nasigned long
miałaby niezdefiniowane zachowanie.źródło
Podążając za odpowiedzią @ AnttiHaapala , przetestowałem kod przy użyciu optymalizacji
/Ox
i stwierdziłem, że to usunie błąd, ponieważ__ftol2_sse
nie 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
__dtoui3
w 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_sse
jest wywoływana w obu konwersjach, dając wynik2147483648
w obu sytuacjach, podejrzenia @zwol były prawidłowe.Szczegóły kompilacji:
W programie Visual Studio:
Wyłączenie
RTC
w Project->
Properties->
Code Generationi ustawienie Sprawdza Podstawowe Runtime aby domyślnie .Włączanie optymalizacji Project
->
Properties->
Optimizationi ustawienie Optymalizacji na / Ox .Z debugerem w
x86
trybie.źródło
getDouble
wyjście z linii i / lub zmienisz ją, aby zwrócić wartość, której kompilator nie może udowodnić, jest stała.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 nauint32_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. tj
0x80000000
.(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 jakcvttsd2si eax, xmm0
(przy użyciu obcinania w kierunku 0, tot
oznacza dodatkowe ).Więc kompilacja jest błędem
double
->unsigned
konwersja 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 x87
fistp
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.źródło