É seguro verificar os valores de ponto flutuante quanto à igualdade de 0?

100

Eu sei que você não pode confiar na igualdade entre valores de tipo duplo ou decimal normalmente, mas estou me perguntando se 0 é um caso especial.

Embora eu possa entender imprecisões entre 0,00000000000001 e 0,00000000000002, 0 em si parece muito difícil de errar, já que não é nada. Se você for impreciso sobre nada, não é mais nada.

Mas não sei muito sobre esse assunto, então não cabe a mim dizer.

double x = 0.0;
return (x == 0.0) ? true : false;

Isso sempre retornará verdadeiro?

Gene Roberts
fonte
69
O operador ternário é redundante nesse código :)
Joel Coehoorn
5
LOL você está certo. Go me
Gene Roberts
Eu não faria isso porque você não sabe como x foi zerado. Se você ainda quiser fazer isso, você provavelmente vai querer arredondar ou o piso x para se livrar do 1e-12 ou algo que possa estar marcado no final.
Rex Logan

Respostas:

115

É seguro esperar que a comparação retornará truese e somente se a variável double tiver um valor de exatamente 0.0(que em seu trecho de código original é, obviamente, o caso). Isso é consistente com a semântica do ==operador. a == bsignifica " aé igual ab ".

Não é seguro (porque não é correto ) esperar que o resultado de algum cálculo seja zero em aritmética dupla (ou mais geralmente, ponto flutuante) sempre que o resultado do mesmo cálculo em matemática pura for zero. Isso ocorre porque quando os cálculos são feitos, o erro de precisão de ponto flutuante aparece - um conceito que não existe na aritmética de números reais na matemática.

Daniel Daranas
fonte
51

Se você precisar fazer muitas comparações de "igualdade", pode ser uma boa ideia escrever uma pequena função auxiliar ou método de extensão no .NET 3.5 para comparar:

public static bool AlmostEquals(this double double1, double double2, double precision)
{
    return (Math.Abs(double1 - double2) <= precision);
}

Isso pode ser usado da seguinte maneira:

double d1 = 10.0 * .1;
bool equals = d1.AlmostEquals(0.0, 0.0000001);
Dirk Vollmar
fonte
4
Você pode estar tendo um erro de cancelamento subtrativo ao comparar double1 e double2, caso esses números tenham valores muito próximos uns dos outros. Gostaria de remover o Math.Abs ​​e verificar cada ramificação individualmente d1> = d2 - eed1 <= d2 + e
Theodore Zographos
"Como o Epsilon define a expressão mínima de um valor positivo cujo intervalo é próximo a zero, a margem de diferença entre dois valores semelhantes deve ser maior do que o Epsilon. Normalmente, é muitas vezes maior do que o Epsilon. Por isso, recomendamos que você faça não use Epsilon ao comparar valores Double para igualdade. " - msdn.microsoft.com/en-gb/library/ya2zha7s(v=vs.110).aspx
Rafael Costa
15

Para sua amostra simples, esse teste está correto. Mas e quanto a isso:

bool b = ( 10.0 * .1 - 1.0 == 0.0 );

Lembre-se de que .1 é um decimal repetido em binário e não pode ser representado exatamente. Em seguida, compare isso com este código:

double d1 = 10.0 * .1; // make sure the compiler hasn't optimized the .1 issue away
bool b = ( d1 - 1.0 == 0.0 );

Vou deixá-lo fazer um teste para ver os resultados reais: é mais provável que você se lembre dessa forma.

Joel Coehoorn
fonte
5
Na verdade, isso retorna verdadeiro por algum motivo (no LINQPad, pelo menos).
Alexey Romanov
De que é o "problema .1" que você fala?
Teejay
14

Na entrada MSDN para Double.Equals :

Precisão em comparações

O método Equals deve ser usado com cuidado, porque dois valores aparentemente equivalentes podem ser desiguais devido à precisão diferente dos dois valores. O exemplo a seguir relata que o valor de Double .3333 e o Double retornado pela divisão de 1 por 3 são desiguais.

...

Em vez de comparar por igualdade, uma técnica recomendada envolve a definição de uma margem aceitável de diferença entre dois valores (como 0,01% de um dos valores). Se o valor absoluto da diferença entre os dois valores for menor ou igual a essa margem, é provável que a diferença seja devido a diferenças na precisão e, portanto, os valores provavelmente serão iguais. O exemplo a seguir usa essa técnica para comparar 0,33333 e 1/3, os dois valores Double que o exemplo de código anterior considerou desiguais.

Além disso, consulte Double.Epsilon .

Stu Mackellar
fonte
1
Também é possível que valores não muito equivalentes sejam comparados como iguais. Seria de se esperar que se x.Equals(y), então (1/x).Equals(1/y), mas esse não é o caso se xé 0e yé 1/Double.NegativeInfinity. Esses valores são declarados iguais, embora seus recíprocos não.
supercat
@supercat: Eles são equivalentes. E eles não têm recíprocos. Você poderia executar o teste novamente com x = 0e y = 0, e ainda assim descobriria 1/x != 1/y.
Ben Voigt
@BenVoigt: Com xe ycomo tipo double? Como você compara os resultados para torná-los relatados desiguais? Observe que 1 / 0,0 não é NaN.
supercat
@supercat: Ok, é uma das coisas que o IEEE-754 dá errado. (Primeiro, isso 1.0/0.0não é NaN como deveria ser, pois o limite não é único. Em segundo lugar, que os infinitos se comparam iguais entre si, sem prestar atenção aos graus do infinito)
Ben Voigt
@BenVoigt: Se o zero foi o resultado da multiplicação de dois números muito pequenos, então a divisão de 1,0 deve resultar em um valor que compara maior do que qualquer número dos pequenos números com o mesmo sinal, e menor do que qualquer número se um dos pequenos os números tinham sinais opostos. IMHO, IEEE-754 seria melhor se tivesse um zero sem sinal, mas infinitesimais positivo e negativo.
supercat
6

O problema surge quando você está comparando diferentes tipos de implementação de valor de ponto flutuante, por exemplo, comparando float com double. Mas com o mesmo tipo, não deve ser um problema.

float f = 0.1F;
bool b1 = (f == 0.1); //returns false
bool b2 = (f == 0.1F); //returns true

O problema é que o programador às vezes esquece que a conversão de tipo implícito (double to float) está acontecendo para a comparação e isso resulta em um bug.

Yogee
fonte
3

Se o número foi atribuído diretamente ao float ou double, então é seguro testar contra zero ou qualquer número inteiro que pode ser representado em 53 bits para um double ou 24 bits para um float.

Ou, colocando de outra forma, você sempre pode atribuir um valor inteiro a um duplo e, em seguida, comparar o duplo de volta ao mesmo inteiro e ter a garantia de que será igual.

Você também pode começar atribuindo um número inteiro e fazer comparações simples continuarem a funcionar, aderindo à adição, subtração ou multiplicação por números inteiros (assumindo que o resultado seja inferior a 24 bits para um float abd 53 bits para um duplo). Portanto, você pode tratar floats e doubles como inteiros sob certas condições controladas.

Kevin Gale
fonte
Eu concordo com sua declaração em geral (e votou positivamente), mas acredito que realmente depende se a implementação de ponto flutuante IEEE 754 é usada ou não. E acredito que todo computador "moderno" usa IEEE 754, pelo menos para armazenamento de flutuadores (existem regras de arredondamento estranhas que diferem).
Mark Lakata,
2

Não, não está bem. Os chamados valores desnormalizados (subnormais), quando comparados iguais a 0,0, seriam comparados como falsos (diferentes de zero), mas quando usados ​​em uma equação seriam normalizados (se tornariam 0,0). Portanto, usar isso como um mecanismo para evitar uma divisão por zero não é seguro. Em vez disso, adicione 1.0 e compare com 1.0. Isso garantirá que todos os subnormais sejam tratados como zero.


fonte
Subnormais também são conhecidos como denormais
Manuel
Os subnormais não se tornam iguais a zero quando usados, embora possam ou não produzir o mesmo resultado, dependendo da operação exata.
wnoise
-2

Tente isso e você descobrirá que == não é confiável para double / float.
double d = 0.1 + 0.2; bool b = d == 0.3;

Aqui está a resposta do Quora.

Rickyuu
fonte
-4

Na verdade, acho melhor usar os seguintes códigos para comparar um valor duplo com 0,0:

double x = 0.0;
return (Math.Abs(x) < double.Epsilon) ? true : false;

O mesmo para flutuador:

float x = 0.0f;
return (Math.Abs(x) < float.Epsilon) ? true : false;
David.Chu.ca
fonte
5
Não. Dos documentos de double.Epsilon: "Se você criar um algoritmo personalizado que determina se dois números de ponto flutuante podem ser considerados iguais, você deve usar um valor que seja maior do que a constante Epsilon para estabelecer a margem de diferença absoluta aceitável para os dois valores serem considerados iguais. (Normalmente, essa margem de diferença é muitas vezes maior do que Epsilon.) "
Alastair Maw
1
@AlastairMaw isso se aplica à verificação de igualdade de duas duplas de qualquer tamanho. Para verificar a igualdade com zero, double.Epsilon está bem.
jwg
4
Não, não é não . É muito provável que o valor que você chegou por meio de algum cálculo esteja muitas vezes longe de zero em epsilon, mas ainda deve ser considerado zero. Você não consegue magicamente um monte de precisão extra em seu resultado intermediário de algum lugar, só porque ele está perto de zero.
Alastair Maw
4
Por exemplo: (1,0 / 5,0 + 1,0 / 5,0 - 1,0 / 10,0 - 1,0 / 10,0 - 1,0 / 10,0 - 1,0 / 10,0) <double.Epsilon == false (e consideravelmente em termos de magnitude: 2,78E-17 vs 4,94E -324)
Alastair Maw
então qual é a precisão recomendada, se double.Epsilon não está ok? 10 vezes de epsilon ok? 100 vezes?
liang