C # Float expression: dziwne zachowanie podczas rzutowania wyniku float na int

130

Mam następujący prosty kod:

int speed1 = (int)(6.2f * 10);
float tmp = 6.2f * 10;
int speed2 = (int)tmp;

speed1i speed2powinien mieć tę samą wartość, ale w rzeczywistości mam:

speed1 = 61
speed2 = 62

Wiem, że powinienem prawdopodobnie użyć Math.Round zamiast rzutowania, ale chciałbym zrozumieć, dlaczego wartości są różne.

Spojrzałem na wygenerowany kod bajtowy, ale oprócz sklepu i obciążenia, kody operacyjne są takie same.

Próbowałem też tego samego kodu w javie i poprawnie otrzymałem 62 i 62.

Czy ktoś może to wyjaśnić?

Edycja: W prawdziwym kodzie nie jest to bezpośrednio 6,2f * 10, ale wywołanie funkcji * stała. Mam następujący kod bajtowy:

dla speed1:

IL_01b3:  ldloc.s    V_8
IL_01b5:  callvirt   instance float32 myPackage.MyClass::getSpeed()
IL_01ba:  ldc.r4     10.
IL_01bf:  mul
IL_01c0:  conv.i4
IL_01c1:  stloc.s    V_9

dla speed2:

IL_01c3:  ldloc.s    V_8
IL_01c5:  callvirt   instance float32 myPackage.MyClass::getSpeed()
IL_01ca:  ldc.r4     10.
IL_01cf:  mul
IL_01d0:  stloc.s    V_10
IL_01d2:  ldloc.s    V_10
IL_01d4:  conv.i4
IL_01d5:  stloc.s    V_11

widzimy, że operandy są zmiennoprzecinkowymi i jedyną różnicą jest stloc/ldloc.

Jeśli chodzi o maszynę wirtualną, próbowałem z Mono / Win7, Mono / MacOS i .NET / Windows, z tymi samymi wynikami.

Baalrukh
źródło
9
Domyślam się, że jedna z operacji została wykonana z pojedynczą precyzją, a druga z podwójną precyzją. Jeden z nich zwrócił wartości nieco mniejsze niż 62, a więc przy obcięciu do liczby całkowitej daje 61.
Gabe
2
Są to typowe problemy z precyzją zmiennoprzecinkową.
TJHeuvel
3
Wypróbowanie tego na .Net / WinXP, .Net / Win7, Mono / Ubuntu i Mono / OSX daje wyniki dla obu wersji systemu Windows, ale 62 dla speed1 i speed2 w obu wersjach Mono. Dzięki @BoltClock
Eugen Rieck
6
Panie Lippert ... jest pan w pobliżu?
vc 74
6
Analizator wyrażeń stałych kompilatora nie wygrywa tutaj żadnych nagród. Najwyraźniej obcina 6,2f w pierwszym wyrażeniu, nie ma dokładnej reprezentacji w podstawie 2, więc kończy się na 6,199999. Ale nie robi tego w drugim wyrażeniu, prawdopodobnie udaje mu się jakoś zachować podwójną precyzję. W przeciwnym razie jest to normalne dla kursu, spójność zmiennoprzecinkowa nigdy nie jest problemem. To nie zostanie naprawione, znasz obejście.
Hans Passant

Odpowiedzi:

170

Przede wszystkim zakładam, że wiesz, że 6.2f * 10to nie jest dokładnie 62 ze względu na zaokrąglenie zmiennoprzecinkowe (w rzeczywistości jest to wartość 61,99999809265137 wyrażona jako a double) i że twoje pytanie dotyczy tylko tego, dlaczego dwa pozornie identyczne obliczenia dają niewłaściwą wartość.

Odpowiedź jest taka, że ​​w przypadku (int)(6.2f * 10), bierzesz doublewartość 61,99999809265137 i skracasz ją do liczby całkowitej, co daje 61.

W przypadku float f = 6.2f * 10, bierzesz podwójną wartość 61,99999809265137 i zaokrąglasz do najbliższej float, czyli 62. Następnie skracasz ją floatdo liczby całkowitej i otrzymujesz 62.

Ćwiczenie: wyjaśnij wyniki następującej sekwencji działań.

double d = 6.2f * 10;
int tmp2 = (int)d;
// evaluate tmp2

Aktualizacja: jak zauważono w komentarzach, wyrażenie 6.2f * 10jest formalnie a, floatponieważ drugi parametr ma niejawną konwersję do, floatktóra jest lepsza niż niejawna konwersja do double.

Faktyczny problem polega na tym, że kompilator ma prawo (ale nie jest to wymagane) do użycia półproduktu, który jest dokładniejszy niż typ formalny (sekcja 11.2.2) . Dlatego widzisz różne zachowanie w różnych systemach: w wyrażeniu (int)(6.2f * 10)kompilator ma możliwość zachowania wartości 6.2f * 10w bardzo precyzyjnej formie pośredniej przed konwersją na int. Jeśli tak, to wynik wynosi 61. Jeśli tak nie jest, wynikiem jest 62.

W drugim przykładzie jawne przypisanie do floatwymusza zaokrąglenie przed konwersją na liczbę całkowitą.

Raymond Chen
źródło
6
Nie jestem pewien, czy to rzeczywiście odpowiada na pytanie. Dlaczego (int)(6.2f * 10)przyjmuje doublewartość, jak fokreśla, że ​​jest to float? Myślę, że główny punkt (wciąż bez odpowiedzi) jest tutaj.
ken2k
1
Myślę, że to kompilator robi to, ponieważ jest to literał float * int dosłowny, kompilator zdecydował, że może używać najlepszego typu liczbowego i aby zachować precyzję, odszedł na double (być może). (również wyjaśniałoby to, że IL jest taki sam)
George Duckett
5
Słuszna uwaga. Tak 6.2f * 10naprawdę floatnie double. Myślę, że kompilator optymalizuje produkt pośredni, na co zezwala ostatni akapit 11.1.6 .
Raymond Chen
3
Ma tę samą wartość (wartość to 61,99999809265137). Różnica polega na ścieżce, którą podąża wartość, aby stać się liczbą całkowitą. W jednym przypadku przechodzi bezpośrednio do liczby całkowitej, aw innym floatnajpierw przechodzi konwersję.
Raymond Chen
38
Odpowiedź Raymonda jest oczywiście całkowicie poprawna. Zwracam uwagę, że kompilator C # i kompilator jit mogą w dowolnym momencie używać większej precyzji i robić to niespójnie . I faktycznie, właśnie to robią. To pytanie pojawiło się dziesiątki razy w witrynie StackOverflow; ostatni przykład można znaleźć na stackoverflow.com/questions/8795550/… .
Eric Lippert
11

Opis

Liczby zmiennoprzecinkowe rzadko są dokładne. 6.2fjest czymś w rodzaju 6.1999998.... Jeśli rzucisz to na int, obcięte zostanie i to * 10 da 61.

Sprawdź DoubleConverterzajęcia Jon Skeets . Dzięki tej klasie możesz naprawdę wizualizować wartość liczby zmiennoprzecinkowej jako ciąg. Doublei floatobie są liczbami zmiennoprzecinkowymi, a liczba dziesiętna nie (jest to liczba stałoprzecinkowa).

Próba

DoubleConverter.ToExactString((6.2f * 10))
// output 61.9999980926513671875

Więcej informacji

dknaack
źródło
5

Spójrz na IL:

IL_0000:  ldc.i4.s    3D              // speed1 = 61
IL_0002:  stloc.0
IL_0003:  ldc.r4      00 00 78 42     // tmp = 62.0f
IL_0008:  stloc.1
IL_0009:  ldloc.1
IL_000A:  conv.i4
IL_000B:  stloc.2

Kompilator redukuje wyrażenia stałych czasu kompilacji do ich stałej wartości i myślę, że dokonuje błędnego przybliżenia w pewnym momencie, gdy konwertuje stałą na int. W przypadku speed2konwersji ta nie jest wykonywana przez kompilator, ale przez CLR i wydają się stosować inne zasady ...

Thomas Levesque
źródło
1

Domyślam się, że 6.2frzeczywistą reprezentacją z precyzją zmiennoprzecinkową jest 6.1999999while, 62fprawdopodobnie podobny do 62.00000001. (int)rzutowanie zawsze obcina wartość dziesiętną , dlatego otrzymujesz takie zachowanie.

EDYCJA : Zgodnie z komentarzami przeformułowałem zachowanie intrzutowania do znacznie dokładniejszej definicji.

Pomiędzy
źródło
Rzutowanie na intwartość ucina wartość dziesiętną, ale nie zaokrągla.
Jim D'Angelo
@James D'Angelo: Przepraszam, angielski nie jest moim głównym językiem. Nie znałem dokładnego słowa, więc zdefiniowałem to zachowanie jako „zaokrąglanie w dół w przypadku liczb dodatnich”, które zasadniczo opisuje to samo zachowanie. Ale tak, rozumiem, obcięte to dokładne określenie.
okresie między
nie ma problemu, to tylko symantyka, ale może powodować kłopoty, jeśli ktoś zacznie myśleć float-> intobejmuje zaokrąglanie. = D
Jim D'Angelo
1

Skompilowałem i zdemasowałem ten kod (na Win7 / .NET 4.0). Wydaje mi się, że kompilator ocenia zmienne wyrażenie stałe jako double.

int speed1 = (int)(6.2f * 10);
   mov         dword ptr [rbp+8],3Dh       //result is precalculated (61)

float tmp = 6.2f * 10;
   movss       xmm0,dword ptr [000004E8h]  //precalculated (float format, xmm0=0x42780000 (62.0))
   movss       dword ptr [rbp+0Ch],xmm0 

int speed2 = (int)tmp;
   cvttss2si   eax,dword ptr [rbp+0Ch]     //instrunction converts float to Int32 (eax=62)
   mov         dword ptr [rbp+10h],eax 
Rodji
źródło
0

Singlezawiera tylko 7 cyfr i podczas rzutowania Int32na kompilator obcina wszystkie cyfry zmiennoprzecinkowe. Podczas konwersji jedna lub więcej znaczących cyfr może zostać utracona.

Int32 speed0 = (Int32)(6.2f * 100000000); 

daje wynik 619999980, więc (Int32) (6.2f * 10) daje 61.

Inaczej jest, gdy mnoży się dwa Single, w takim przypadku nie ma operacji obcięcia, a jedynie przybliżenie.

Zobacz http://msdn.microsoft.com/en-us/library/system.single.aspx

Massimo Zerbini
źródło
-4

Czy jest jakiś powód, dla którego wpisujesz rzutowanie intzamiast analizowania?

int speed1 = (int)(6.2f * 10)

przeczytałby

int speed1 = Int.Parse((6.2f * 10).ToString()); 

Różnica jest prawdopodobnie doublezwiązana z zaokrąglaniem: jeśli rzucasz do siebie, prawdopodobnie otrzymasz coś w rodzaju 61,78426.

Zwróć uwagę na następujące dane wyjściowe

int speed1 = (int)(6.2f * 10);//61
double speed2 = (6.2f * 10);//61.9999980926514

Dlatego otrzymujesz różne wartości!

Neo
źródło
1
Int.Parseprzyjmuje ciąg jako parametr.
ken2k
Możesz analizować tylko ciągi, myślę, że masz na myśli, dlaczego nie używasz System.Convert
vc 74