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.


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.


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


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 ...


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.


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 

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


-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!


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.