Mam następujący prosty kod:
int speed1 = (int)(6.2f * 10);
float tmp = 6.2f * 10;
int speed2 = (int)tmp;
speed1
i speed2
powinien 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.
źródło
Odpowiedzi:
Przede wszystkim zakładam, że wiesz, że
6.2f * 10
to nie jest dokładnie 62 ze względu na zaokrąglenie zmiennoprzecinkowe (w rzeczywistości jest to wartość 61,99999809265137 wyrażona jako adouble
) 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)
, bierzeszdouble
wartość 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ższejfloat
, czyli 62. Następnie skracasz jąfloat
do 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 * 10
jest formalnie a,float
ponieważ drugi parametr ma niejawną konwersję do,float
która jest lepsza niż niejawna konwersja dodouble
.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ści6.2f * 10
w bardzo precyzyjnej formie pośredniej przed konwersją naint
. Jeśli tak, to wynik wynosi 61. Jeśli tak nie jest, wynikiem jest 62.W drugim przykładzie jawne przypisanie do
float
wymusza zaokrąglenie przed konwersją na liczbę całkowitą.źródło
(int)(6.2f * 10)
przyjmujedouble
wartość, jakf
określa, że jest tofloat
? Myślę, że główny punkt (wciąż bez odpowiedzi) jest tutaj.6.2f * 10
naprawdęfloat
niedouble
. Myślę, że kompilator optymalizuje produkt pośredni, na co zezwala ostatni akapit 11.1.6 .float
najpierw przechodzi konwersję.Opis
Liczby zmiennoprzecinkowe rzadko są dokładne.
6.2f
jest czymś w rodzaju6.1999998...
. Jeśli rzucisz to na int, obcięte zostanie i to * 10 da 61.Sprawdź
DoubleConverter
zajęcia Jon Skeets . Dzięki tej klasie możesz naprawdę wizualizować wartość liczby zmiennoprzecinkowej jako ciąg.Double
ifloat
obie 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
źródło
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 przypadkuspeed2
konwersji ta nie jest wykonywana przez kompilator, ale przez CLR i wydają się stosować inne zasady ...źródło
Domyślam się, że
6.2f
rzeczywistą reprezentacją z precyzją zmiennoprzecinkową jest6.1999999
while,62f
prawdopodobnie podobny do62.00000001
.(int)
rzutowanie zawsze obcina wartość dziesiętną , dlatego otrzymujesz takie zachowanie.EDYCJA : Zgodnie z komentarzami przeformułowałem zachowanie
int
rzutowania do znacznie dokładniejszej definicji.źródło
int
wartość ucina wartość dziesiętną, ale nie zaokrągla.float
->int
obejmuje zaokrąglanie. = DSkompilował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
źródło
Single
zawiera tylko 7 cyfr i podczas rzutowaniaInt32
na 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
źródło
Czy jest jakiś powód, dla którego wpisujesz rzutowanie
int
zamiast analizowania?int speed1 = (int)(6.2f * 10)
przeczytałby
int speed1 = Int.Parse((6.2f * 10).ToString());
Różnica jest prawdopodobnie
double
zwią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!
źródło
Int.Parse
przyjmuje ciąg jako parametr.