Comportamento estranho ao converter um float para int em c #

Eu tenho o seguinte código simples:

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

speed1 e speed2 devem ter o mesmo valor, mas na verdade eu tenho:

 speed1 = 61 speed2 = 62 

Eu sei que provavelmente deveria usar Math.Round em vez de lançar, mas gostaria de entender por que os valores são diferentes.

Eu olhei para o bytecode gerado, mas com exceção de uma loja e uma carga, os opcodes são os mesmos.

Eu também tentei o mesmo código em java, e eu obtenho corretamente 62 e 62.

Alguém pode explicar isso?

Edit: No código real, não é diretamente 6.2f * 10, mas uma chamada de function * uma constante. Eu tenho o seguinte bytecode:

para a velocidade 1:

 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 velocidade 2:

 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 operandos são floats e que a única diferença é o stloc / ldloc

Quanto à máquina virtual, eu tentei com Mono / Win7, Mono / MacOS e .NET / Windows, com os mesmos resultados

Primeiro de tudo, eu suponho que você saiba que 6.2f * 10 não é exatamente 62 devido ao arredondamento de ponto flutuante (é realmente o valor 61.99999809265137 quando expresso como um double ) e que sua pergunta é apenas sobre por que dois cálculos aparentemente idênticos resultam no valor errado.

A resposta é que, no caso de (int)(6.2f * 10) , você está obtendo o valor double 61.99999809265137 e truncando-o para um inteiro, que produz 61.

No caso de float f = 6.2f * 10 , você está tomando o valor double 61.99999809265137 e arredondando para o float mais próximo, que é 62. Você então trunca esse float para um inteiro, e o resultado é 62.

Exercício: Explique os resultados da seguinte sequência de operações.

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

Atualização: Conforme observado nos comentários, a expressão 6.2f * 10 é formalmente um float já que o segundo parâmetro tem uma conversão implícita para float que é melhor que a conversão implícita para double .

A questão real é que o compilador é permitido (mas não obrigatório) para usar um intermediário que é maior precisão do que o tipo formal . É por isso que você vê um comportamento diferente em sistemas diferentes: Na expressão (int)(6.2f * 10) , o compilador tem a opção de manter o valor 6.2f * 10 em uma forma intermediária de alta precisão antes de converter para int . Em caso afirmativo, o resultado é 61. Se isso não ocorrer, o resultado será 62.

No segundo exemplo, a atribuição explícita para float força o arredondamento a ocorrer antes da conversão para inteiro.

Descrição

Números flutuantes raramente são exatos. 6.2f é algo como 6.1999998... Se você converter isso para um int, ele truncará e isso resultará em 10.

Confira a class Jon Skeets DoubleConverter . Com essa class você pode realmente visualizar o valor de um número flutuante como string. Double e float são ambos números flutuantes , decimal não é (é um número de ponto fixo).

Amostra

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

Mais Informações

  • Classe do DoubleConverter de Jon Skeet
  • Assert.AreEqual () com System.Double ficando realmente confuso
  • O que todo cientista da computação deve saber sobre aritmética de ponto flutuante

Olhe para o 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 

O compilador reduz as expressões constantes de tempo de compilation ao seu valor constante, e acho que faz uma aproximação errada em algum ponto quando ele converte a constante em int . No caso de speed2 , esta conversão é feita não pelo compilador, mas pelo CLR, e eles parecem aplicar regras diferentes …

Meu palpite é que a representação real 6.2f com precisão de float é 6.1999999 enquanto 62f é provavelmente algo semelhante a 62.00000001 . (int) casting sempre trunca o valor decimal, e é por isso que você obtém esse comportamento.

EDIT : De acordo com comentários eu reformulei o comportamento de int casting para uma definição muito mais precisa.

Single mantém apenas 7 dígitos e ao lançá-lo para um Int32 o compilador trunca todos os dígitos do ponto flutuante. Durante a conversão, um ou mais dígitos significativos podem ser perdidos.

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

dá o resultado de 619999980 assim (Int32) (6.2f * 10) dá 61.

É diferente quando dois únicos são multiplicados, nesse caso não há operação de truncamento, mas apenas aproximação.

Consulte http://msdn.microsoft.com/pt-br/library/system.single.aspx

Eu compilei e desmontei este código (no Win7 / .NET 4.0). Eu acho que o compilador avalia a expressão constante flutuante como duplo.

 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 

Existe uma razão pela qual você está digitando o tipo para int vez de analisar?

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

então ler

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

A diferença provavelmente está relacionada ao arredondamento: se você lançar para o double , provavelmente obterá algo como 61.78426.

Por favor, observe a seguinte saída

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

É por isso que você está recebendo valores diferentes!