Quão perigoso é comparar valores de ponto flutuante?

391

Eu sei UIKitusosCGFloat devido ao sistema de coordenadas independente da resolução.

Mas cada vez que eu quero verificar se, por exemplo, frame.origin.xé 0que me faz sentir doente:

if (theView.frame.origin.x == 0) {
    // do important operation
}

Não é CGFloatvulnerável a falsos positivos ao comparar com ==, <=, >=, <, >? É um ponto flutuante e eles têm problemas de imprecisão: 0.0000000000041por exemplo.

Está Objective-Clidando com isso internamente ao comparar ou pode acontecer que um origin.xque seja lido como zero não se compare com 0verdadeiro?

Membro orgulhoso
fonte

Respostas:

466

Primeiro de tudo, os valores de ponto flutuante não são "aleatórios" em seu comportamento. A comparação exata pode e faz sentido em muitos usos no mundo real. Mas se você usar o ponto flutuante, precisará estar ciente de como ele funciona. Errar do lado de assumir que o ponto flutuante funciona como números reais fará com que você codifique rapidamente. Ao assumir que os resultados de ponto flutuante têm um grande número aleatório associado a eles (como a maioria das respostas sugeridas aqui), você obtém um código que parece funcionar no início, mas acaba tendo erros de grande magnitude e cantos quebrados.

Primeiro de tudo, se você deseja programar com ponto flutuante, leia este:

O que todo cientista da computação deve saber sobre aritmética de ponto flutuante

Sim, leia tudo. Se isso for um fardo demais, você deve usar números inteiros / ponto fixo para seus cálculos até ter tempo para lê-lo. :-)

Agora, com isso dito, os maiores problemas com comparações exatas de ponto flutuante se resumem a:

  1. O fato de que muitos valores que você pode escrever na fonte, ou ler com scanfou strtod, não existem como valores de ponto flutuante e se silenciosamente convertidos para a aproximação mais próxima. Era sobre isso que a resposta de demon9733 estava falando.

  2. O fato de muitos resultados serem arredondados devido à falta de precisão suficiente para representar o resultado real. Um exemplo fácil de como você pode ver isso é adicionar x = 0x1fffffee y = 1flutuar. Aqui, xpossui 24 bits de precisão na mantissa (ok) e ytem apenas 1 bit, mas quando você os adiciona, os bits não estão em locais sobrepostos e o resultado precisaria de 25 bits de precisão. Em vez disso, é arredondado (para 0x2000000no modo de arredondamento padrão).

  3. O fato de muitos resultados serem arredondados devido à necessidade de infinitos lugares para o valor correto. Isso inclui resultados racionais, como 1/3 (que você conhece do decimal, que ocupa infinitamente muitas casas), mas também 1/10 (que também ocupa infinitamente muitas casas no binário, já que 5 não é uma potência de 2), além de resultados irracionais, como a raiz quadrada de qualquer coisa que não seja um quadrado perfeito.

  4. Arredondamento duplo. Em alguns sistemas (particularmente x86), as expressões de ponto flutuante são avaliadas com maior precisão do que seus tipos nominais. Isso significa que, quando um dos tipos de arredondamento acima ocorrer, você terá duas etapas de arredondamento, primeiro o arredondamento do resultado para o tipo de maior precisão, depois o arredondamento para o tipo final. Como exemplo, considere o que acontece em decimal se você arredondar 1,49 para um número inteiro (1), versus o que acontece se você primeiro arredondar para uma casa decimal (1,5) e depois arredondar o resultado para um número inteiro (2). Essa é realmente uma das áreas mais desagradáveis ​​para lidar com ponto flutuante, já que o comportamento do compilador (especialmente para compiladores de bugs e não conformes como o GCC) é imprevisível.

  5. Funções transcendentes ( trig, exp, log, etc.) não são especificados para ter resultados correctamente arredondados; o resultado é especificado apenas como correto em uma unidade no último local de precisão (geralmente chamado de 1ulp ).

Ao escrever um código de ponto flutuante, lembre-se do que está fazendo com os números que podem causar resultados inexatos e faça comparações de acordo. Muitas vezes, faz sentido comparar com um "epsilon", mas esse epsilon deve ser baseado na magnitude dos números que você está comparando , e não em uma constante absoluta. (Nos casos em que um epsilon constante absoluto funcionaria, isso é fortemente indicativo de que o ponto fixo, e não o ponto flutuante, é a ferramenta certa para o trabalho!)

Edit: Em particular, uma verificação epsilon relativa à magnitude deve ser algo como:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))

De onde FLT_EPSILONé a constante float.h(substitua-a DBL_EPSILONpor doubles ou LDBL_EPSILONpor long doubles) e Ké uma constante que você escolhe para que o erro acumulado de seus cálculos seja definitivamente limitado por Kunidades em último lugar (e se você não tiver certeza de que recebeu o erro cálculo limite certo, façaK algumas vezes maior do que o que seus cálculos dizem que deveria ser).

Por fim, observe que, se você usar isso, poderá ser necessário algum cuidado especial próximo de zero, pois FLT_EPSILONnão faz sentido para os anormais. Uma solução rápida seria fazê-lo:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)

e da mesma forma substitua DBL_MINse estiver usando duplas.

R .. GitHub PARE DE AJUDAR O GELO
fonte
25
fabs(x+y)é problemático se xe y(pode) ter sinal diferente. Ainda assim, uma boa resposta contra a maré de comparações entre cultos de carga.
22712 Daniel Fischer
27
Se xe ytiver sinal diferente, não há problema. O lado direito será "muito pequeno", mas como xe ycom sinais diferentes, eles não devem ser iguais. (A menos que sejam tão pequenos que não sejam anormais, mas o segundo caso seja detectado)
R .. GitHub PARE DE AJUDAR O GELO
4
Estou curioso sobre a sua afirmação: "especialmente para compiladores de bugs e não conformes como o GCC". É realmente um bug do GCC e também não está em conformidade?
Nicolás Ozimica
3
Como a pergunta é marcada como iOS, vale a pena notar que os compiladores da Apple (tanto o clang quanto o gcc da Apple) sempre usaram FLT_EVAL_METHOD = 0 e tentam ser completamente rigorosos sobre não carregar precisão excessiva. Se você encontrar alguma violação disso, envie relatórios de erros.
Stephen Canon
17
"Primeiro, os valores de ponto flutuante não são" aleatórios "em seu comportamento. A comparação exata pode e faz sentido em muitos usos no mundo real." - Apenas duas frases e já ganhou um +1! Essa é uma das hipóteses mais perturbadoras que as pessoas fazem ao trabalhar com pontos flutuantes.
Christian Rau
36

Como 0 é exatamente representável como um número de ponto flutuante IEEE754 (ou usando qualquer outra implementação de números fp com os quais já trabalhei), a comparação com 0 é provavelmente segura. Você pode ser mordido, no entanto, se o seu programa computar um valor (como theView.frame.origin.x) que você tenha motivos para acreditar que deve ser 0, mas que sua computação não pode garantir que seja 0.

Para esclarecer um pouco, um cálculo como:

areal = 0.0

(a menos que seu idioma ou sistema esteja quebrado) criará um valor tal que (areal == 0.0) retorne verdadeiro, mas outro cálculo, como

areal = 1.386 - 2.1*(0.66)

não deve.

Se você pode garantir a si mesmo que seus cálculos produzem valores que são 0 (e não apenas que eles produzam valores que devem ser 0), você pode ir em frente e comparar os valores de fp com 0. Se você não pode se assegurar no grau exigido , é melhor seguir a abordagem usual da "igualdade tolerada".

Nos piores casos, a comparação descuidada dos valores de FP pode ser extremamente perigosa: pense em aviônicos, orientação de armas, operações de usinas, navegação de veículos, quase qualquer aplicativo em que a computação encontre o mundo real.

Para Angry Birds, não é tão perigoso.

Marca de alto desempenho
fonte
11
Na verdade, 1.30 - 2*(0.65)é um exemplo perfeito de uma expressão que obviamente avalia como 0,0 se o seu compilador implementa a IEEE 754, porque as dobras representadas como 0.65e 1.30têm os mesmos significandos, e a multiplicação por duas é obviamente exata.
Pascal Cuoq
7
Ainda recebendo representante deste, mudei o segundo trecho de exemplo.
High Performance Mark
22

Eu quero dar uma resposta um pouco diferente das outras. Eles são ótimos para responder à sua pergunta como indicado, mas provavelmente não para o que você precisa saber ou qual é o seu problema real.

Ponto flutuante nos gráficos é bom! Mas quase não há necessidade de comparar os carros alegóricos diretamente. Por que você precisaria fazer isso? Os gráficos usam flutuadores para definir intervalos. E comparar se um flutuador está dentro de um intervalo também definido por flutuadores é sempre bem definido e apenas precisa ser consistente, não exato ou preciso! Contanto que um pixel (que também é um intervalo!) Possa ser atribuído, é tudo o que os gráficos precisam.

Portanto, se você quiser testar se o seu ponto está fora de um intervalo de [0..width [], tudo bem. Apenas certifique-se de definir a inclusão de maneira consistente. Por exemplo, sempre defina inside is (x> = 0 && x <width). O mesmo vale para testes de interseção ou batida.

No entanto, se você estiver abusando de uma coordenada gráfica como algum tipo de sinalizador, como, por exemplo, para ver se uma janela está encaixada ou não, você não deve fazer isso. Use um sinalizador booleano que é separado da camada de apresentação gráfica.

starmole
fonte
13

Comparar com zero pode ser uma operação segura, desde que o zero não seja um valor calculado (conforme observado na resposta acima). A razão para isso é que zero é um número perfeitamente representável em ponto flutuante.

Falando de valores perfeitamente representáveis, você obtém 24 bits de alcance em uma noção de potência de dois (precisão única). Portanto, 1, 2, 4 são perfeitamente representáveis, assim como 0,5, 0,25 e 0,125. Contanto que todos os seus bits importantes estejam em 24 bits, você será dourado. Portanto, 10.625 pode ser representado com precisão.

Isso é ótimo, mas rapidamente desmoronará sob pressão. Dois cenários vêm à mente: 1) Quando um cálculo está envolvido. Não confie nesse sqrt (3) * sqrt (3) == 3. Simplesmente não será assim. E provavelmente não estará dentro de um épsilon, como sugerem algumas das outras respostas. 2) Quando qualquer NPOT (NPOT) estiver envolvido. Portanto, pode parecer estranho, mas 0,1 é uma série infinita em binário e, portanto, qualquer cálculo envolvendo um número como esse será impreciso desde o início.

(Ah, e a pergunta original mencionou comparações com zero. Não se esqueça que -0,0 também é um valor de ponto flutuante perfeitamente válido.)

JHumphrey
fonte
11

[A 'resposta correta' encobre a seleção K. A seleção Kacaba sendo tão ad-hoc quanto a seleção, VISIBLE_SHIFTmas a seleção Ké menos óbvia, porque, ao contrário VISIBLE_SHIFT, não se baseia em nenhuma propriedade de exibição. Assim, escolha seu veneno - selecione Kou selecione VISIBLE_SHIFT. Esta resposta defende a seleção VISIBLE_SHIFTe, em seguida, demonstra a dificuldade em selecionar K]

Precisamente devido a erros de arredondamento, você não deve usar a comparação de valores 'exatos' para operações lógicas. No seu caso específico de uma posição em uma exibição visual, não importa se a posição é 0,0 ou 0,0000000003 - a diferença é invisível aos olhos. Portanto, sua lógica deve ser algo como:

#define VISIBLE_SHIFT    0.0001        // for example
if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }

No entanto, no final, 'invisível aos olhos' dependerá das propriedades da sua exibição. Se você pode limitar o display (você deve conseguir); escolha VISIBLE_SHIFTser uma fração desse limite superior.

Agora, a 'resposta certa' se baseia, Kentão vamos explorar a escolha K. A 'resposta correta' acima diz:

K é uma constante que você escolhe para que o erro acumulado de seus cálculos seja definitivamente limitado por K unidades em último lugar (e se você não tiver certeza de ter acertado o cálculo de erro, faça K algumas vezes maior do que seus cálculos diga que deveria ser)

Então nós precisamos K. Se conseguir Ké mais difícil, menos intuitivo do que selecionar o meu, VISIBLE_SHIFTentão você decidirá o que funciona para você. Para descobrir K, vamos escrever um programa de teste que analise vários Kvalores para que possamos ver como ele se comporta. Deveria ser óbvio como escolher K, se a 'resposta certa' for utilizável. Não?

Vamos usar como detalhes da 'resposta certa':

if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)

Vamos apenas tentar todos os valores de K:

#include <math.h>
#include <float.h>
#include <stdio.h>

void main (void)
{
  double x = 1e-13;
  double y = 0.0;

  double K = 1e22;
  int i = 0;

  for (; i < 32; i++, K = K/10.0)
    {
      printf ("K:%40.16lf -> ", K);

      if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)
        printf ("YES\n");
      else
        printf ("NO\n");
    }
}
ebg@ebg$ gcc -o test test.c
ebg@ebg$ ./test
K:10000000000000000000000.0000000000000000 -> YES
K: 1000000000000000000000.0000000000000000 -> YES
K:  100000000000000000000.0000000000000000 -> YES
K:   10000000000000000000.0000000000000000 -> YES
K:    1000000000000000000.0000000000000000 -> YES
K:     100000000000000000.0000000000000000 -> YES
K:      10000000000000000.0000000000000000 -> YES
K:       1000000000000000.0000000000000000 -> NO
K:        100000000000000.0000000000000000 -> NO
K:         10000000000000.0000000000000000 -> NO
K:          1000000000000.0000000000000000 -> NO
K:           100000000000.0000000000000000 -> NO
K:            10000000000.0000000000000000 -> NO
K:             1000000000.0000000000000000 -> NO
K:              100000000.0000000000000000 -> NO
K:               10000000.0000000000000000 -> NO
K:                1000000.0000000000000000 -> NO
K:                 100000.0000000000000000 -> NO
K:                  10000.0000000000000000 -> NO
K:                   1000.0000000000000000 -> NO
K:                    100.0000000000000000 -> NO
K:                     10.0000000000000000 -> NO
K:                      1.0000000000000000 -> NO
K:                      0.1000000000000000 -> NO
K:                      0.0100000000000000 -> NO
K:                      0.0010000000000000 -> NO
K:                      0.0001000000000000 -> NO
K:                      0.0000100000000000 -> NO
K:                      0.0000010000000000 -> NO
K:                      0.0000001000000000 -> NO
K:                      0.0000000100000000 -> NO
K:                      0.0000000010000000 -> NO

Ah, então K deve ser 1e16 ou maior se eu quiser que 1e-13 seja 'zero'.

Então, eu diria que você tem duas opções:

  1. Faça um cálculo epsilon simples usando seu julgamento de engenharia pelo valor de 'epsilon', como sugeri. Se você estiver criando gráficos e 'zero' for uma 'mudança visível', examine seus recursos visuais (imagens, etc.) e julgue o que é epsilon.
  2. Não tente fazer cálculos de ponto flutuante até ler a referência da resposta que não é de carga (e obter seu doutorado no processo) e, em seguida, use seu julgamento não intuitivo para selecionar K.
GoZoner
fonte
10
Um aspecto da independência de resolução é que você não pode dizer com certeza o que é uma "mudança visível" em tempo de compilação. O que é invisível em uma tela super HD pode muito bem ser óbvio em uma tela minúscula. Deve-se pelo menos torná-lo uma função do tamanho da tela. Ou nomeie outra coisa.
Romain
11
Mas pelo menos a seleção de 'deslocamento visível' é baseada em propriedades de exibição (ou quadro) de fácil compreensão - ao contrário das <respostas corretas>, Kque são difíceis e não intuitivas de selecionar.
GoZoner 28/08/16
5

A pergunta correta: como comparar pontos no Cocoa Touch?

A resposta correta: CGPointEqualToPoint ().

Uma pergunta diferente: dois valores calculados são iguais?

A resposta postada aqui: Eles não são.

Como verificar se eles estão próximos? Se você quiser verificar se eles estão próximos, não use CGPointEqualToPoint (). Mas não verifique se eles estão próximos. Faça algo que faça sentido no mundo real, como verificar se um ponto está além de uma linha ou se está dentro de uma esfera.

Michael T.
fonte
4

A última vez que verifiquei o padrão C, não havia necessidade de operações de ponto flutuante em duplas (total de 64 bits, mantissa de 53 bits) para ter precisão acima dessa precisão. No entanto, algum hardware pode fazer as operações em registradores de maior precisão, e o requisito foi interpretado como significando nenhum requisito para limpar bits de ordem inferior (além da precisão dos números sendo carregados nos registradores). Assim, você pode obter resultados inesperados de comparações como essa, dependendo do que resta nos registros de quem dormiu lá por último.

Dito isto, e apesar dos meus esforços para eliminá-lo sempre que o vejo, o equipamento em que trabalho tem muito código C que é compilado usando gcc e executado no linux, e não notamos nenhum desses resultados inesperados há muito tempo . Não tenho idéia se isso ocorre porque o gcc está limpando os bits de baixa ordem para nós, os registradores de 80 bits não são usados ​​para essas operações em computadores modernos, o padrão foi alterado ou o quê. Gostaria de saber se alguém pode citar capítulos e versículos.

Membrana de Lucas
fonte
1

Você pode usar esse código para comparar float com zero:

if ((int)(theView.frame.origin.x * 100) == 0) {
    // do important operation
}

Isso será comparado com precisão de 0,1, o suficiente para o CGFloat nesse caso.

Igor
fonte
A conversão para intsem seguro theView.frame.origin.xestá dentro / próximo do intervalo de intleads que leva ao comportamento indefinido (UB) - ou, nesse caso, 1/100 do intervalo de int.
chux - Restabelece Monica 16/04
Não há absolutamente nenhuma razão para converter em número inteiro como este. Como disse o chux, existe o potencial de UB a partir de valores fora da faixa; e em algumas arquiteturas isso será significativamente mais lento do que apenas fazer o cálculo em ponto flutuante. Por fim, multiplicar por 100 como esse compara com precisão 0,01, e não 0,1.
Sneftel
0
-(BOOL)isFloatEqual:(CGFloat)firstValue secondValue:(CGFloat)secondValue{

BOOL isEqual = NO;

NSNumber *firstValueNumber = [NSNumber numberWithDouble:firstValue];
NSNumber *secondValueNumber = [NSNumber numberWithDouble:secondValue];

isEqual = [firstValueNumber isEqualToNumber:secondValueNumber];

return isEqual;

}

Abbas Mulani
fonte
0

Estou usando a seguinte função de comparação para comparar um número de casas decimais:

bool compare(const double value1, const double value2, const int precision)
{
    int64_t magnitude = static_cast<int64_t>(std::pow(10, precision));
    int64_t intValue1 = static_cast<int64_t>(value1 * magnitude);
    int64_t intValue2 = static_cast<int64_t>(value2 * magnitude);
    return intValue1 == intValue2;
}

// Compare 9 decimal places:
if (compare(theView.frame.origin.x, 0, 9)) {
    // do important operation
}
jeans
fonte
-6

Eu diria que a coisa certa é declarar cada número como um objeto e depois definir três coisas nesse objeto: 1) um operador de igualdade. 2) um método setAcceptableDifference. 3) o próprio valor. O operador de igualdade retornará true se a diferença absoluta de dois valores for menor que o valor definido como aceitável.

Você pode subclassificar o objeto para se adequar ao problema. Por exemplo, barras redondas de metal entre 1 e 2 polegadas podem ser consideradas de diâmetro igual se seus diâmetros diferirem em menos de 0,0001 polegadas. Então você chamaria setAcceptableDifference com o parâmetro 0.0001 e usaria o operador de igualdade com confiança.

John White
fonte
11
Esta não é uma boa resposta. Primeiro, a "coisa do objeto" não faz nada para resolver o seu problema. E segundo, sua implementação real de "igualdade" não é de fato a correta.
Tom Swirly
3
Tom, talvez você pense novamente sobre a "coisa do objeto". Com números reais, representados com alta precisão, a igualdade raramente acontece. Mas a idéia de igualdade de alguém pode ser adaptada, se for o seu caso. Seria melhor se houvesse um operador substituível 'aproximadamente igual', mas não existe.
John