C # Float-Ausdruck: Seltsames Verhalten beim Umwandeln des Ergebnisses float in int

128

Ich habe den folgenden einfachen Code:

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

speed1und speed2sollte den gleichen Wert haben, aber in der Tat habe ich:

speed1 = 61
speed2 = 62

Ich weiß, ich sollte wahrscheinlich Math.Round anstelle von Casting verwenden, aber ich würde gerne verstehen, warum die Werte unterschiedlich sind.

Ich habe mir den generierten Bytecode angesehen, aber bis auf einen Speicher und eine Ladung sind die Opcodes dieselben.

Ich habe den gleichen Code auch in Java ausprobiert und erhalte 62 und 62 korrekt.

Kann das jemand erklären?

Bearbeiten: Im realen Code ist es nicht direkt 6.2f * 10, sondern ein Funktionsaufruf * eine Konstante. Ich habe folgenden Bytecode:

für 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

für 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

wir können sehen, dass Operanden Floats sind und dass der einzige Unterschied der ist stloc/ldloc.

Was die virtuelle Maschine betrifft, habe ich es mit Mono / Win7, Mono / MacOS und .NET / Windows versucht, mit den gleichen Ergebnissen.

Baalrukh
quelle
9
Ich vermute, dass eine der Operationen mit einfacher Genauigkeit ausgeführt wurde, während die andere mit doppelter Genauigkeit ausgeführt wurde. Einer von ihnen gab Werte zurück, die etwas kleiner als 62 waren, und ergab 61, wenn er auf eine ganze Zahl abgeschnitten wurde.
Gabe
2
Dies sind typische Probleme mit der Gleitkommapräzision.
TJHeuvel
3
Wenn Sie dies unter .Net / WinXP, .Net / Win7, Mono / Ubuntu und Mono / OSX versuchen, erhalten Sie Ergebnisse für beide Windows-Versionen, aber 62 für speed1 und speed2 in beiden Mono-Versionen. Danke @BoltClock
Eugen Rieck
6
Herr Lippert ... Sie herum?
VC 74
6
Der Bewerter für konstante Ausdrücke des Compilers gewinnt hier keine Preise. Es ist klar, dass es 6.2f im ersten Ausdruck abschneidet, es hat keine exakte Darstellung in Basis 2 und endet als 6.199999. Aber tut dies nicht im 2. Ausdruck, wahrscheinlich indem es gelingt, es irgendwie in doppelter Genauigkeit zu halten. Dies ist ansonsten selbstverständlich, Gleitkommakonsistenz ist nie ein Problem. Dies wird nicht behoben, Sie kennen die Problemumgehung.
Hans Passant

Antworten:

168

Zunächst 6.2f * 10gehe ich davon aus, dass Sie wissen, dass dies aufgrund der Gleitkomma-Rundung nicht genau 62 ist (es ist tatsächlich der Wert 61.99999809265137, ausgedrückt als a double), und dass Ihre Frage nur lautet, warum zwei scheinbar identische Berechnungen zu einem falschen Wert führen.

Die Antwort ist, dass Sie im Fall von (int)(6.2f * 10)den doubleWert 61.99999809265137 nehmen und ihn auf eine Ganzzahl kürzen, die 61 ergibt.

Im Fall von nehmen float f = 6.2f * 10Sie den doppelten Wert 61.99999809265137 und runden auf den nächsten Wert float(62). Sie kürzen diesen Wert dann floatauf eine Ganzzahl, und das Ergebnis ist 62.

Übung: Erläutern Sie die Ergebnisse der folgenden Abfolge von Operationen.

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

Update: Wie in den Kommentaren erwähnt, ist der Ausdruck 6.2f * 10formal ein, floatda der zweite Parameter eine implizite Konvertierung aufweist, in floatdie besser ist als die implizite Konvertierung in double.

Das eigentliche Problem ist, dass der Compiler ein Zwischenprodukt verwenden darf (aber nicht muss), das eine höhere Genauigkeit als der formale Typ aufweist (Abschnitt 11.2.2) . Aus diesem Grund sehen Sie auf verschiedenen Systemen ein unterschiedliches Verhalten: Im Ausdruck (int)(6.2f * 10)hat der Compiler die Möglichkeit, den Wert 6.2f * 10vor der Konvertierung in eine hochpräzise Zwischenform zu halten int. Wenn dies der Fall ist, ist das Ergebnis 61. Wenn dies nicht der Fall ist, ist das Ergebnis 62.

Im zweiten Beispiel floaterzwingt die explizite Zuweisung zu , dass die Rundung vor der Konvertierung in eine Ganzzahl erfolgt.

Raymond Chen
quelle
6
Ich bin mir nicht sicher, ob dies tatsächlich die Frage beantwortet. Warum wird (int)(6.2f * 10)der doubleWert wie fangegeben verwendet float? Ich denke, der Hauptpunkt (immer noch unbeantwortet) ist hier.
Ken2k
1
Ich denke, es ist der Compiler, der das tut, da es sich um Float-Literal * int-Literal handelt, hat der Compiler entschieden, dass es kostenlos ist, den besten numerischen Typ zu verwenden, und um Präzision zu sparen, ist er (vielleicht) doppelt so hoch. (würde auch erklären, dass IL dasselbe ist)
George Duckett
5
Guter Punkt. Die Art von 6.2f * 10ist eigentlich floatnicht double. Ich denke, der Compiler optimiert das Intermediate, wie es der letzte Absatz von 11.1.6 erlaubt .
Raymond Chen
3
Es hat den gleichen Wert (der Wert ist 61.99999809265137). Der Unterschied ist der Weg, den der Wert auf dem Weg zur Ganzzahl nimmt. In einem Fall geht es direkt zu einer Ganzzahl, und in einem anderen Fall wird floatzuerst eine Konvertierung durchgeführt.
Raymond Chen
38
Die Antwort von Raymond hier ist natürlich völlig richtig. Ich stelle fest, dass sowohl der C # -Compiler als auch der JIT-Compiler jederzeit präziser und uneinheitlicher arbeiten dürfen . Und genau das tun sie. Diese Frage ist bei StackOverflow Dutzende Male aufgetaucht. Ein aktuelles Beispiel finden Sie unter stackoverflow.com/questions/8795550/… .
Eric Lippert
11

Beschreibung

Schwebende Zahlen sind selten genau. 6.2fist so etwas wie 6.1999998.... Wenn Sie dies in ein int umwandeln, wird es abgeschnitten und diese * 10 ergibt 61.

Schauen Sie sich die Jon Skeets- DoubleConverterKlasse an. Mit dieser Klasse können Sie den Wert einer schwebenden Zahl wirklich als Zeichenfolge visualisieren. Doubleund floatsind beide Gleitkommazahlen , Dezimalzahlen nicht (es ist eine Festkommazahl).

Stichprobe

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

Mehr Informationen

dknaack
quelle
5

Schauen Sie sich die IL an:

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

Der Compiler reduziert konstante Ausdrücke zur Kompilierungszeit auf ihren konstanten Wert, und ich denke, er macht irgendwann eine falsche Annäherung, wenn er die Konstante in konvertiert int. Im Fall von speed2wird diese Konvertierung nicht vom Compiler, sondern von der CLR vorgenommen, und sie scheinen unterschiedliche Regeln anzuwenden ...

Thomas Levesque
quelle
1

Ich vermute, dass eine 6.2fechte Darstellung mit Float-Präzision 6.1999999dabei 62fwahrscheinlich etwas Ähnliches ist 62.00000001. (int)Beim Casting wird der Dezimalwert immer abgeschnitten, sodass Sie dieses Verhalten erhalten.

EDIT : Laut Kommentaren habe ich das Verhalten des intCastings in eine viel genauere Definition umformuliert .

Zwischen
quelle
Beim Umwandeln auf a wird intder Dezimalwert abgeschnitten, nicht gerundet.
Jim D'Angelo
@ James D'Angelo: Entschuldigung, Englisch ist nicht meine Hauptsprache. Ich kannte das genaue Wort nicht und definierte das Verhalten als "Abrunden beim Umgang mit positiven Zahlen", das im Grunde das gleiche Verhalten beschreibt. Aber ja, Punkt genommen, abgeschnitten ist das genaue Wort dafür.
Zwischen dem
Kein Problem, es ist nur symantisch, kann aber Probleme verursachen, wenn jemand anfängt zu denken float-> intbeinhaltet das Runden. = D
Jim D'Angelo
1

Ich habe diesen Code kompiliert und disassembliert (unter Win7 / .NET 4.0). Ich denke, dass der Compiler den schwebenden konstanten Ausdruck als doppelt bewertet.

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
quelle
0

Singleenthält nur 7 Ziffern und beim Umwandeln in einen schneidet Int32der Compiler alle Gleitkommaziffern ab. Während der Konvertierung können eine oder mehrere signifikante Ziffern verloren gehen.

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

ergibt das Ergebnis von 619999980, also ergibt (Int32) (6.2f * 10) 61.

Es ist anders, wenn zwei Single multipliziert werden. In diesem Fall gibt es keine abgeschnittene Operation, sondern nur eine Annäherung.

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

Massimo Zerbini
quelle
-4

Gibt es einen Grund, warum Sie Typ Casting intanstelle von Parsing verwenden?

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

würde dann lesen

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

Der Unterschied liegt wahrscheinlich in der Rundung: Wenn Sie auf werfen, erhalten doubleSie wahrscheinlich so etwas wie 61.78426.

Bitte beachten Sie die folgende Ausgabe

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

Deshalb bekommen Sie unterschiedliche Werte!

Neo
quelle
1
Int.ParseNimmt einen String als Parameter.
Ken2k
Sie können nur Zeichenfolgen analysieren, ich denke, Sie meinen, warum verwenden Sie nicht System.Convert
vc 74