Expresión flotante de C #: comportamiento extraño al convertir el resultado flotante a int

128

Tengo el siguiente código simple:

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

speed1y speed2debería tener el mismo valor, pero de hecho, tengo:

speed1 = 61
speed2 = 62

Sé que probablemente debería usar Math.Round en lugar de lanzar, pero me gustaría entender por qué los valores son diferentes.

Miré el bytecode generado, pero excepto una tienda y una carga, los códigos de operación son los mismos.

También probé el mismo código en Java, y obtuve correctamente 62 y 62.

¿Alguien puede explicar esto?

Editar: en el código real, no es directamente 6.2f * 10 sino una función llamada * una constante. Tengo el siguiente bytecode:

para 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

para 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

podemos ver que los operandos son flotadores y que la única diferencia es el stloc/ldloc.

En cuanto a la máquina virtual, probé con Mono / Win7, Mono / MacOS y .NET / Windows, con los mismos resultados.

Baalrukh
fuente
9
Supongo que una de las operaciones se realizó con precisión simple, mientras que la otra se realizó con precisión doble. Uno de ellos devolvió valores ligeramente inferiores a 62 y, por lo tanto, produjo 61 al truncar a un entero.
Gabe
2
Estos son problemas típicos de precisión de punto flotante.
TJHeuvel
3
Probar esto en .Net / WinXP, .Net / Win7, Mono / Ubuntu y Mono / OSX da sus resultados para ambas versiones de Windows, pero 62 para speed1 y speed2 en ambas versiones Mono. Gracias @BoltClock
Eugen Rieck
66
Sr. Lippert ... ¿estás por aquí?
vc 74
66
El evaluador de expresiones constantes del compilador no está ganando ningún premio aquí. Claramente, está truncando 6.2f en la primera expresión, no tiene una representación exacta en la base 2, por lo que termina como 6.199999. Pero no lo hace en la segunda expresión, probablemente logrando mantenerlo con doble precisión de alguna manera. De lo contrario, esto es normal, la consistencia de coma flotante nunca es un problema. Esto no se va a arreglar, ya sabes la solución.
Hans Passant

Respuestas:

168

En primer lugar, supongo que sabe que 6.2f * 10no es exactamente 62 debido al redondeo de coma flotante (en realidad es el valor 61.99999809265137 cuando se expresa como a double) y que su pregunta es solo por qué dos cálculos aparentemente idénticos resultan en un valor incorrecto.

La respuesta es que, en el caso de (int)(6.2f * 10), está tomando el doublevalor 61.99999809265137 y truncándolo a un número entero, lo que produce 61.

En el caso de float f = 6.2f * 10, está tomando el valor doble 61.99999809265137 y redondeando al más cercano float, que es 62. Luego lo trunca floata un entero, y el resultado es 62.

Ejercicio: explique los resultados de la siguiente secuencia de operaciones.

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

Actualización: Como se señaló en los comentarios, la expresión 6.2f * 10es formalmente una floatya que el segundo parámetro tiene una conversión implícita a la floatcual es mejor que la conversión implícita double.

El problema real es que el compilador tiene permiso (pero no es obligatorio) para usar un intermedio que es de mayor precisión que el tipo formal (sección 11.2.2) . Es por eso que ve un comportamiento diferente en diferentes sistemas: en la expresión (int)(6.2f * 10), el compilador tiene la opción de mantener el valor 6.2f * 10en una forma intermedia de alta precisión antes de convertir int. Si lo hace, entonces el resultado es 61. Si no lo hace, entonces el resultado es 62.

En el segundo ejemplo, la asignación explícita a floatobliga al redondeo a realizarse antes de la conversión a entero.

Raymond Chen
fuente
66
No estoy seguro de que esto realmente responda la pregunta. ¿Por qué está (int)(6.2f * 10)tomando el doublevalor, como fespecifica que es un float? Creo que el punto principal (aún sin respuesta) está aquí.
ken2k
1
Creo que es el compilador el que está haciendo eso, ya que es literal flotante * int literal, el compilador ha decidido que es libre de usar el mejor tipo numérico y, para ahorrar precisión, se ha duplicado (tal vez). (también explicaría que IL es igual)
George Duckett
55
Buen punto. El tipo de 6.2f * 10es en realidad float, no double. Creo que el compilador está optimizando el intermedio, como lo permite el último párrafo de 11.1.6 .
Raymond Chen el
3
Tiene el mismo valor (el valor es 61.99999809265137). La diferencia es el camino que toma el valor para convertirse en un número entero. En un caso, va directamente a un número entero, y en otro pasa floatprimero por una conversión.
Raymond Chen el
38
La respuesta de Raymond aquí es, por supuesto, completamente correcta. Observo que tanto el compilador de C # como el compilador de jit pueden usar más precisión en cualquier momento y hacerlo de manera inconsistente . Y de hecho, hacen exactamente eso. Esta pregunta ha surgido docenas de veces en StackOverflow; consulte stackoverflow.com/questions/8795550/… para ver un ejemplo reciente.
Eric Lippert
11

Descripción

Los números flotantes son raramente exactos. 6.2fes algo así como 6.1999998.... Si lanza esto a un int, lo truncará y esto * 10 da como resultado 61.

Echa un vistazo a la DoubleConverterclase Jon Skeets . Con esta clase realmente puede visualizar el valor de un número flotante como una cadena. Doubley floatson ambos números flotantes , el decimal no es (es un número de punto fijo).

Muestra

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

Más información

dknaack
fuente
5

Mira el 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

El compilador reduce las expresiones constantes en tiempo de compilación a su valor constante, y creo que hace una aproximación incorrecta en algún momento cuando convierte la constante a int. En el caso de speed2, esta conversión no la realiza el compilador, sino el CLR, y parece que aplican reglas diferentes ...

Thomas Levesque
fuente
1

Supongo que 6.2fla representación real con precisión flotante es 6.1999999mientras que 62fes probablemente algo similar a 62.00000001. (int)la conversión siempre trunca el valor decimal, por eso es que obtienes ese comportamiento.

EDITAR : Según los comentarios, he reformulado el comportamiento del intcasting a una definición mucho más precisa.

Entre
fuente
Lanzar a un inttrunca el valor decimal, no se redondea.
Jim D'Angelo
@ James D'Angelo: Lo siento, el inglés no es mi idioma principal. No sabía la palabra exacta, así que definí el comportamiento como "redondear hacia abajo cuando se trata de números positivos", que básicamente describe el mismo comportamiento. Pero sí, punto tomado, truncar es la palabra exacta para ello.
Entre el
no hay problema, es solo una sintaxis, pero puede causar problemas si alguien comienza a pensar float-> intimplica redondeo. = D
Jim D'Angelo
1

Compilé y desensamblé este código (en Win7 / .NET 4.0). Supongo que el compilador evalúa la expresión constante flotante como doble.

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

Singlemantiene solo 7 dígitos y cuando lo envía Int32al compilador trunca todos los dígitos de coma flotante. Durante la conversión, se pueden perder uno o más dígitos significativos.

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

da el resultado de 619999980, entonces (Int32) (6.2f * 10) da 61.

Es diferente cuando se multiplican dos Single, en ese caso no hay operación truncada sino solo aproximación.

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

Massimo Zerbini
fuente
-4

¿Hay alguna razón por la que estás escribiendo en intlugar de analizar?

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

entonces leería

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

La diferencia probablemente tenga que ver con el redondeo: si lanzas a double lanzas probablemente obtendrás algo como 61.78426.

Tenga en cuenta la siguiente salida

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

¡Es por eso que obtienes valores diferentes!

Neo
fuente
1
Int.Parsetoma una cadena como parámetro.
ken2k
Solo puede analizar cadenas, supongo que quiere decir por qué no usa System.Convert
vc 74