O ponto flutuante está sempre OK?

Hoje mesmo me deparei com softwares de terceiros que estamos usando e em seu código de exemplo havia algo ao longo destas linhas:

// Defined in somewhere.h static const double BAR = 3.14; // Code elsewhere.cpp void foo(double d) { if (d == BAR) ... } 

Estou ciente do problema com pontos flutuantes e sua representação, mas me fez pensar se há casos em que float == float estaria bem? Eu não estou pedindo quando poderia funcionar, mas quando faz sentido e funciona.

Além disso, que tal uma chamada como foo(BAR) ? Será que isso sempre se compara à medida que ambos usam a mesma static const BAR ?

Existem duas maneiras de responder a essa pergunta:

  1. Existem casos em que float == float dá o resultado correto?
  2. Há casos em que float == float é aceitável?

A resposta para (1) é: Sim, às vezes. Mas vai ser frágil, o que leva à resposta de (2): Não. Não faça isso. Você está implorando por erros bizarros no futuro.

Quanto a uma chamada do formulário foo(BAR) : Nesse caso particular a comparação retornará verdadeira, mas quando você está escrevendo foo você não sabe (e não deve depender) como é chamado. Por exemplo, chamar foo(BAR) ficará bem, mas foo(BAR * 2.0 / 2.0) (ou mesmo talvez foo(BAR * 1.0) dependendo de quanto o compilador otimiza as coisas) irá quebrar. Você não deveria estar confiando no chamador não realizar nenhuma aritmética!

Resumindo, embora a == b funcione em alguns casos, você realmente não deve confiar nele. Mesmo se você puder garantir a semântica das chamadas hoje, talvez você não possa garanti-las na próxima semana, então poupe um pouco de dor e não use == .

Na minha opinião, float == float nunca é * OK porque é praticamente inatingível.

* Para valores pequenos de nunca.

Sim, você tem a garantia de que números inteiros, incluindo 0,0, são comparados com ==

Claro que você tem que ser um pouco cuidadoso com a forma como você obteve o número inteiro, a atribuição é segura, mas o resultado de qualquer cálculo é suspeito

ps há um conjunto de números reais que têm uma reprodução perfeita como um float (pense em 1/2, 1/4 1/8 etc) mas você provavelmente não sabe de antemão que você tem um desses.

Só para esclarecer. É garantido pelo IEEE 754 que as representações float de inteiros (números inteiros) dentro do intervalo, são exatas.

 float a=1.0; float b=1.0; a==b // true 

Mas você tem que ter cuidado como você consegue os números inteiros

 float a=1.0/3.0; a*3.0 == 1.0 // not true !! 

As outras respostas explicam muito bem porque usar == para números de ponto flutuante é perigoso. Acabei de encontrar um exemplo que ilustra muito bem esses perigos, acredito.

Na plataforma x86, você pode obter resultados de ponto flutuante estranhos para alguns cálculos, que não são devido a problemas de arredondamento inerentes aos cálculos que você executa. Este programa C simples, às vezes, imprime “erro”:

 #include  void test(double x, double y) { const double y2 = x + 1.0; if (y != y2) printf("error\n"); } void main() { const double x = .012; const double y = x + 1.0; test(x, y); } 

O programa basicamente calcula

 x = 0.012 + 1.0; y = 0.012 + 1.0; 

(apenas espalhe por duas funções e com variables ​​intermediárias), mas a comparação ainda pode gerar resultados falsos!

A razão é que, na plataforma x86, os programas geralmente usam o FPU x87 para cálculos de ponto flutuante. O x87 calcula internamente com uma precisão maior que o double regular, portanto, valores double precisam ser arredondados quando são armazenados na memory. Isso significa que uma viagem de ida e volta x87 -> RAM -> x87 perde precisão e, portanto, os resultados dos cálculos diferem dependendo de se os resultados intermediários foram transmitidos via RAM ou se todos permaneceram em registros FPU. Esta é, naturalmente, uma decisão do compilador, então o bug só se manifesta para certos compiladores e configurações de otimização :-(.

Para detalhes, veja o bug do GCC: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=323

Bastante assustador …

Nota adicional:

Bugs desse tipo geralmente serão bastante complicados de depurar, porque os diferentes valores se tornam os mesmos quando atingem a RAM.

Então, se por exemplo você estender o programa acima para realmente imprimir os padrões de bits de y e y2 logo após compará-los, você obterá o mesmo valor exato . Para imprimir o valor, ele tem que ser carregado na RAM para ser passado para alguma function de impressão como printf , e isso fará a diferença desaparecer …

Perfeito para valores integrais, mesmo em formatos de ponto flutuante

Mas a resposta curta é: “Não, não use ==”.

Ironicamente, o formato de ponto flutuante funciona “perfeitamente”, ou seja, com precisão exata, ao operar em valores integrais dentro do intervalo do formato. Isto significa que se você ficar com valores duplos , você obtém inteiros perfeitamente bons com pouco mais de 50 bits, dando-lhe cerca de + – 4.500.000.000.000.000, ou 4.5 quadrilhões.

Na verdade, é assim que o JavaScript funciona internamente, e é por isso que o JavaScript pode fazer coisas como + e - em números realmente grandes, mas só pode << e >> em números de 32 bits.

Estritamente falando, você pode comparar exatamente sums e produtos de números com representações precisas. Aqueles seriam todos os inteiros, mais frações compostas de 1 / 2n termos. Então, um loop incrementando por n + 0.25, n + 0.50, ou n + 0.75 seria bom, mas nenhuma das outras 96 frações decimais com 2 dígitos.

Portanto, a resposta é: enquanto a igualdade exata pode, em teoria, fazer sentido em casos estreitos, é melhor evitar.

O único caso em que eu uso == (ou != ) Para floats é o seguinte:

 if (x != x) { // Here x is guaranteed to be Not a Number } 

e devo admitir que sou culpado de usar Não Um Número como uma constante mágica de ponto flutuante (usando numeric_limits::quiet_NaN() em C ++).

Não há nenhum ponto em comparar números de ponto flutuante para igualdade estrita. Os números de ponto flutuante foram projetados com limites de precisão relativos previsíveis. Você é responsável por saber que precisão esperar deles e de seus algoritmos.

Tentarei fornecer um exemplo mais ou menos real de testes legítimos, significativos e úteis para a igualdade de float.

 #include  #include  /* let's try to numerically solve a simple equation F(x)=0 */ double F(double x) { return 2*cos(x) - pow(1.2, x); } /* I'll use a well-known, simple&slow but extremely smart method to do this */ double bisection(double range_start, double range_end) { double a = range_start; double d = range_end - range_start; int counter = 0; while(a != a+d) // <-- WHOA!! { d /= 2.0; if(F(a)*F(a+d) > 0) /* test for same sign */ a = a+d; ++counter; } printf("%d iterations done\n", counter); return a; } int main() { /* we must be sure that the root can be found in [0.0, 2.0] */ printf("F(0.0)=%.17f, F(2.0)=%.17f\n", F(0.0), F(2.0)); double x = bisection(0.0, 2.0); printf("the root is near %.17f, F(%.17f)=%.17f\n", x, x, F(x)); } 

Prefiro não explicar o método de bissecção usado em si, mas enfatizo a condição de parada. Ele tem exatamente a forma discutida: (a == a+d) onde ambos os lados são floats: a é a nossa aproximação atual da raiz da equação e d é a nossa precisão atual. Dada a pré-condição do algoritmo – que deve haver uma raiz entre range_start e range_end – nós garantimos em cada iteração que a raiz fica entre a e a+d enquanto d é reduzida pela metade a cada passo, encolhendo os limites.

E então, depois de várias iterações, d se torna tão pequeno que, durante a adição, ele é arredondado para zero! Isto é, a+d acaba por estar mais perto de a então para qualquer outro float ; e assim o FPU arredonda para o valor mais próximo: para o próprio. Isso pode ser facilmente ilustrado pelo cálculo em uma máquina de calcular hipotética; deixe-a ter uma mantissa decimal de 4 dígitos e um grande intervalo de expoente. Então qual resultado a máquina deve dar para 2.131e+02 + 7.000e-3 ? A resposta exata é 213.107 , mas nossa máquina não pode representar esse número; tem que contornar isso. E 213.107 é muito mais próximo de 213.1 que de 213.2 – então o resultado arredondado torna-se 2.131e+02 – o pequeno summand desapareceu, arredondado para zero. Exatamente o mesmo é garantido para acontecer em alguma iteração do nosso algoritmo – e nesse ponto não podemos mais continuar. Nós achamos a raiz com a máxima precisão possível.

A conclusão edificante é, aparentemente, que os carros alegóricos são complicados. Eles se parecem tanto com números reais que todo programador é tentado a pensar neles como números reais. Mas eles não são. Eles têm seu próprio comportamento, lembrando um pouco do real , mas não exatamente o mesmo. Você precisa ter muito cuidado com eles, especialmente quando se compara a igualdade.


Atualizar

Revisitando a resposta depois de um tempo, também notei um fato interessante: no algoritmo acima, não é possível usar “algum pequeno número” na condição de parada. Para qualquer escolha do número, haverá inputs que considerarão sua escolha muito grande , causando perda de precisão, e haverá inputs que considerarão sua escolha muito pequena , causando excesso de iterações ou até mesmo entrando em um loop infinito. Discussão detalhada segue.

Você já deve saber que o cálculo não tem noção de um “número pequeno”: para qualquer número real, é possível encontrar infinitamente muitos outros ainda menores. O problema é que um desses “ainda menores” pode ser o que realmente procuramos; pode ser uma raiz da nossa equação. Pior ainda, para equações diferentes, pode haver raízes distintas (por exemplo, 2.51e-8 e 1.38e-8 ), as quais serão aproximadas pelo mesmo número se nossa condição de parada se parecer com d < 1e-6 . Qualquer que seja o "número pequeno" que você escolher, muitas raízes que foram encontradas corretamente na precisão máxima com a == a+d condição de parada a == a+d serão estragadas pelo "épsilon" ser muito grande .

É verdade, porém, que em números de ponto flutuante o expoente tem alcance limitado, então você pode encontrar o menor número FP positivo diferente de zero (por exemplo, 1e-45 denorm para FP único de precisão IEEE 754). Mas é inútil! while (d < 1e-45) {...} fará um loop para sempre, assumindo precisão simples (positiva diferente de zero) d .

Deixando de lado esses casos de limites patológicos, qualquer escolha do "número pequeno" na condição de parada d < eps será muito pequena para muitas equações. Nas equações em que a raiz tem o expoente alto o suficiente, o resultado da subtração de duas mantissas diferindo apenas pelo dígito menos significativo excederá facilmente nosso "épsilon". Por exemplo, com mantissas de 6 dígitos 7.00023e+8 - 7.00022e+8 = 0.00001e+8 = 1.00000e+3 = 1000 , significando que a menor diferença possível entre números com expoente +8 e 5 dígitos da mantissa é .. 1000! Que nunca se encheckboxrá em, digamos, 1e-4 . Para esses números com expoente (relativamente) alto, simplesmente não temos precisão suficiente para ver uma diferença de 1e-4 .

Minha implementação acima também levou em consideração esse último problema, e você pode ver que d é dividido pela metade a cada passo, em vez de ser recalculado como uma diferença (possivelmente enorme no expoente) b . Portanto, se alterarmos a condição de parada para d < eps , o algoritmo não ficará preso em loop infinito com raízes enormes (pode muito bem com (ba) < eps ), mas ainda executará iterações desnecessárias durante a redução d abaixo da precisão de a .

Esse tipo de raciocínio pode parecer excessivamente teórico e desnecessariamente profundo, mas seu objective é ilustrar novamente a dificuldade dos carros alegóricos. Deve-se ter muito cuidado com sua precisão finita ao escrever operadores aritméticos ao seu redor.

É provavelmente ok se você nunca vai calcular o valor antes de compará-lo. Se você está testando se um número de ponto flutuante é exatamente pi, ou -1, ou 1 e você sabe que os valores limitados estão sendo passados ​​…

Eu também usei algumas vezes quando reescrevemos alguns algoritmos para versões multithread. Eu usei um teste que compara os resultados para a versão única e multithreaded para ter certeza, que ambos dão exatamente o mesmo resultado.

Sim. 1/x será válido a menos que x==0 . Você não precisa de um teste impreciso aqui. 1/0.00000001 está perfeitamente bem. Eu não consigo pensar em nenhum outro caso – você não pode nem conferir tan(x) para x==PI/2

Digamos que você tenha uma function que dimensione uma matriz de flutuantes por um fator constante:

 void scale(float factor, float *vector, int extent) { int i; for (i = 0; i < extent; ++i) { vector[i] *= factor; } } 

Eu assumirei que sua implementação de ponto flutuante pode representar 1.0 e 0.0 exatamente, e que 0.0 é representado por todos os 0 bits.

Se o factor for exatamente 1.0, essa function não funcionará e você poderá retornar sem fazer nenhum trabalho. Se o factor for exatamente 0.0, isso pode ser implementado com uma chamada para memset, que provavelmente será mais rápida do que executar as multiplicações de ponto flutuante individualmente.

A implementação de referência das funções BLAS no netlib usa essas técnicas extensivamente.

Os outros posts mostram onde é apropriado. Eu acho que usando bit-exata compara para evitar o cálculo desnecessário também está bem ..

Exemplo:

 float someFunction (float argument) { // I really want bit-exact comparison here! if (argument != lastargument) { lastargument = argument; cachedValue = very_expensive_calculation (argument); } return cachedValue; } 

Na minha opinião, a comparação por igualdade (ou alguma equivalência) é um requisito na maioria das situações: contêineres padrão C ++ ou algoritmos com um functor de comparação de igualdade implícita, como std :: unordered_set por exemplo, requer que este comparador seja uma relação de equivalência (veja C ++ requisitos nomeados: UnorderedAssociativeContainer ).

Infelizmente, comparar com um epsilon como em abs(a - b) < epsilon não produz uma relação de equivalência, uma vez que perde a transitividade. Este é provavelmente o comportamento indefinido, especificamente dois números de ponto flutuante "quase iguais" podem produzir hashes diferentes; Isso pode colocar o unordered_set em um estado inválido. Pessoalmente, eu usaria == para pontos flutuantes a maior parte do tempo, a menos que qualquer tipo de computação de FPU estivesse envolvida em quaisquer operandos. Com contêineres e algoritmos de contêiner, onde apenas leituras / gravações estão envolvidas, == (ou qualquer relação de equivalência) é a mais segura.

abs(a - b) < epsilon é mais ou menos um critério de convergência semelhante a um limite. Acho essa relação útil se eu precisar verificar se uma identidade matemática é válida entre dois cálculos (por exemplo, PV = nRT ou distância = tempo * velocidade).

Em suma, use == se e somente se nenhum cálculo de ponto flutuante ocorrer; nunca use abs(ab) < e como predicado de igualdade;

Eu diria que comparar carros alegóricos por igualdade seria OK se uma resposta falso-negativa for aceitável .

Suponha, por exemplo, que você tenha um programa que imprima valores de pontos flutuantes na canvas e que, se o valor de ponto flutuante for exatamente igual a M_PI , você gostaria de imprimir “pi”. Se o valor desvia um pequeno bit da representação dupla exata de M_PI , ele imprimirá um valor duplo, que é igualmente válido, mas um pouco menos legível para o usuário.

Eu tenho um programa de desenho que fundamentalmente usa um ponto flutuante para o seu sistema de coordenadas, uma vez que o usuário tem permissão para trabalhar em qualquer granularidade / zoom. A coisa que eles estão desenhando contém linhas que podem ser dobradas em pontos criados por eles. Quando eles arrastam um ponto em cima do outro, eles são mesclados.

Para fazer uma comparação de ponto flutuante “adequada”, eu teria que criar um intervalo dentro do qual considerar os pontos da mesma forma. Como o usuário pode aumentar o zoom ao infinito e trabalhar dentro desse intervalo e, como eu não consegui fazer com que alguém se comprometesse com algum tipo de intervalo, apenas usamos ‘==’ para ver se os pontos são os mesmos. Ocasionalmente, haverá um problema em que os pontos que devem ser exatamente iguais serão removidos em .000000000001 ou algo assim (especialmente em torno de 0,0), mas normalmente funcionam bem. É supostamente difícil unir pontos sem que o snap seja ativado de qualquer maneira … ou pelo menos é assim que a versão original funcionava.

Ele lança o grupo de testes de vez em quando, mas esse é o problema deles: p

De qualquer forma, há um exemplo de um tempo possivelmente razoável para usar ‘==’. A coisa a notar é que a decisão é menos sobre a precisão técnica do que sobre os desejos do cliente (ou falta dele) e conveniência. Não é algo que precisa ser tão preciso assim mesmo. Então, e se dois pontos não se fundirem quando você espera? Não é o fim do mundo e não afetará ‘cálculos’.