Expressão C # Float: comportamento estranho ao converter o resultado float para int

128

Eu tenho o seguinte código simples:

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

speed1e speed2deve ter o mesmo valor, mas, na verdade, tenho:

speed1 = 61
speed2 = 62

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

Eu olhei para o bytecode gerado, mas, exceto uma loja e uma carga, os códigos de operação são os mesmos.

Também tentei o mesmo código em java e 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 função * uma constante. Eu tenho o seguinte 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 os operandos são flutuadores e que a única diferença é a stloc/ldloc.

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

Baalrukh
fonte
9
Meu palpite é que uma das operações foi feita com precisão única enquanto a outra foi feita com precisão dupla. Um deles retornou um valor ligeiramente menor que 62, resultando em 61 ao truncar para um número inteiro.
Gabe
2
Estes são problemas típicos de precisão de ponto flutuante.
TJHeuvel
3
Tentando isso em .Net / WinXP, .Net / Win7, Mono / Ubuntu e Mono / OSX, você obtém resultados para as duas versões do Windows, mas 62 para a velocidade1 e a velocidade2 nas duas versões Mono. Obrigado @BoltClock
Eugen Rieck
6
Sr. Lippert ... você está por aí ??
vc 74
6
O avaliador de expressão constante do compilador não está ganhando nenhum prêmio aqui. Claramente, ele está truncando 6.2f na primeira expressão, não possui uma representação exata na base 2 e, portanto, termina em 6.199999. Mas não o faz na segunda expressão, provavelmente conseguindo mantê-lo em dupla precisão de alguma forma. Caso contrário, este é o padrão, a consistência do ponto flutuante nunca é um problema. Isso não será corrigido, você sabe a solução alternativa.
Hans Passant

Respostas:

168

Primeiro, suponho que você saiba que 6.2f * 10não é exatamente 62 devido ao arredondamento de ponto flutuante (na verdade é o valor 61.99999809265137 quando expresso como a 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á pegando o doublevalor 61.99999809265137 e truncando-o para um número inteiro, que gera 61.

No caso de float f = 6.2f * 10, você está pegando o valor duplo 61.99999809265137 e arredondando para o valor mais próximo float, que é 62. Em seguida, você o trunca floatpara um número 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 uma, floatjá que o segundo parâmetro tem uma conversão implícita na floatqual é melhor que a conversão implícita em double.

O problema real é que o compilador tem permissão (mas não é obrigatório) de usar um intermediário com maior precisão do que o tipo formal (seção 11.2.2) . É 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 * 10em uma forma intermediária de alta precisão antes de converter para int. Se isso acontecer, o resultado será 61. Caso contrário, o resultado será 62.

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

Raymond Chen
fonte
6
Não tenho certeza se isso realmente responde à pergunta. Por que (int)(6.2f * 10)pegar o doublevalor, como fespecifica que é um float? Eu acho que o ponto principal (ainda sem resposta) está aqui.
ken2k
1
Eu acho que é o compilador que está fazendo isso, uma vez que é float literal * int literal, o compilador decidiu que é livre usar o melhor tipo numérico e, para economizar precisão, é usado o dobro (talvez). (também explicaria IL sendo o mesmo)
George Duckett
5
Bom ponto. O tipo de 6.2f * 10é realmente float, não double. Eu acho que o compilador está otimizando o intermediário, conforme permitido pelo último parágrafo do 11.1.6 .
Raymond Chen
3
Ele tem o mesmo valor (o valor é 61.99999809265137). A diferença é o caminho que o valor leva para se tornar um número inteiro. Em um caso, ele vai diretamente para um número inteiro e, em outro, passa por uma floatconversão primeiro.
Raymond Chen
38
A resposta de Raymond aqui é obviamente completamente correta. Observo que o compilador C # e o compilador jit têm permissão para usar mais precisão a qualquer momento e inconsistentemente . E, de fato, eles fazem exatamente isso. Esta pergunta surgiu dezenas de vezes no StackOverflow; consulte stackoverflow.com/questions/8795550/… para um exemplo recente.
Eric Lippert
11

Descrição

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

Confira a DoubleConverteraula de Jon Skeets . Com essa classe, você pode realmente visualizar o valor de um número flutuante como string. Doublee floatsã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

dknaack
fonte
5

Veja 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 expressões constantes em tempo de compilação ao seu valor constante, e acho que faz uma aproximação errada em algum momento quando converte a constante em int. No caso de speed2, essa conversão é feita não pelo compilador, mas pelo CLR, e eles parecem aplicar regras diferentes ...

Thomas Levesque
fonte
1

Meu palpite é que 6.2fa representação real com precisão de flutuação é 6.1999999while 62fprovavelmente é algo semelhante a 62.00000001. (int)a conversão sempre trunca o valor decimal, e é por isso que você obtém esse comportamento.

EDIT : De acordo com os comentários, reformulei o comportamento do intcasting para uma definição muito mais precisa.

Entre
fonte
A conversão para um inttrunca o valor decimal, não é arredondado.
Jim D'Angelo
@ James D'Angelo: Desculpe inglês não é minha língua principal. Não sabia a palavra exata, por isso defini o comportamento como "arredondar para baixo ao lidar com números positivos", que basicamente descreve o mesmo comportamento. Mas sim, ponto de vista, truncar é a palavra exata para isso.
Entre
não tem problema, é apenas uma semântica, mas pode causar problemas se alguém começar a pensar float-> intenvolve arredondamentos. = D
Jim D'Angelo
1

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

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

Singlemantém apenas 7 dígitos e ao convertê-lo em um Int32compilador truncar todos os dígitos de ponto flutuante. Durante a conversão, um ou mais dígitos significativos podem ser perdidos.

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

fornece o resultado de 619999980, então (Int32) (6,2f * 10) fornece 61.

É diferente quando dois Single são multiplicados; nesse caso, não há operação truncada, mas apenas aproximação.

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

Massimo Zerbini
fonte
-4

Existe uma razão pela qual você está digitando a conversão em intvez de analisar?

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

então leria

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

A diferença provavelmente está relacionada ao arredondamento: se você converter para doublevocê, provavelmente obterá algo como 61.78426.

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!

Neo
fonte
1
Int.Parsepega uma string como parâmetro.
ken2k
Você pode cordas única de análise, eu acho que você quer dizer por que você não use System.Convert
vc 74