A comparação da igualdade de números flutuantes engana os desenvolvedores juniores, mesmo que nenhum erro de arredondamento ocorra no meu caso?

31

Por exemplo, quero mostrar uma lista de botões de 0,0,5, ... 5, que salta para cada 0,5. Eu uso um loop for para fazer isso e tenho cores diferentes no botão STANDARD_LINE:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0;i<=MAX;i=i+DIFF){
    button.text=i+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

Nesse caso, não deve haver erros de arredondamento, pois cada valor é exato no IEEE 754. Mas estou lutando para alterá-lo para evitar a comparação de igualdade de ponto flutuante:

var MAX=10;
var STANDARD_LINE=3;

for(var i=0;i<=MAX;i++){
    button.text=i/2.0+'';
    if(i==STANDARD_LINE/2.0){
      button.color='red';
    }
}

Por um lado, o código original é mais simples e encaminhado para mim. Mas há uma coisa que estou considerando: eu == STANDARD_LINE engana colegas juniores? Esconde o fato de que números de ponto flutuante podem ter erros de arredondamento? Depois de ler os comentários deste post:

https://stackoverflow.com/questions/33646148/is-hardcode-float-precise-if-it-can-be-be-represented-by-binary-format-in-ieee-754

parece que muitos desenvolvedores não sabem que alguns números flutuantes são exatos. Devo evitar comparações de igualdade de número flutuante, mesmo que seja válido no meu caso? Ou estou pensando demais sobre isso?

ocomfd
fonte
23
O comportamento dessas duas listagens de código não é equivalente. 3 / 2.0 é 1.5, mas isempre serão números inteiros na segunda listagem. Tente remover o segundo /2.0.
candied_orange
27
Se você absolutamente precisar comparar dois FPs para igualdade (o que não é obrigatório, como outros apontaram em suas respostas finas, já que você pode usar uma comparação de contador de loop com números inteiros), mas se o fez, um comentário deve ser suficiente. Pessoalmente, trabalho com o IEEE FP há muito tempo e ainda ficaria confuso se visse, digamos, uma comparação direta do SPFP sem nenhum tipo de comentário ou qualquer coisa. É apenas um código muito delicado - vale a pena comentar pelo menos uma vez no IMHO.
14
Independentemente de qual você escolher, este é um daqueles casos em que um comentário explicando como e por que é absolutamente essencial. Um desenvolvedor posterior pode nem considerar as sutilezas sem um comentário para chamar sua atenção. Além disso, estou muito distraído com o fato de que buttonnão muda em nenhum lugar do seu loop. Como é acessada a lista de botões? Via índice em array ou algum outro mecanismo? Se for pelo acesso do índice a uma matriz, esse é outro argumento a favor da mudança para números inteiros.
Jpmc26 31/01
9
Escreva esse código. Até que alguém pense que 0,6 seria um tamanho de passo melhor e simplesmente mude essa constante.
tofro
11
"... enganar desenvolvedores juniores" Você também enganará desenvolvedores seniores. Apesar da quantidade de pensamento que você colocou nisso, eles assumem que você não sabia o que estava fazendo e provavelmente o alterarão para a versão inteira.
GrandOpener

Respostas:

116

Eu sempre evitaria operações sucessivas de ponto flutuante, a menos que o modelo que estou computando as exija. A aritmética de ponto flutuante é pouco intuitiva para a maioria e a principal fonte de erros. E dizer os casos em que causa erros daqueles em que não ocorre é uma distinção ainda mais sutil!

Portanto, o uso de flutuadores como contadores de loop é um defeito que está esperando para acontecer e exigiria, no mínimo, um comentário geral explicando por que não há problema em usar 0,5 aqui e que isso depende do valor numérico específico. Nesse ponto, reescrever o código para evitar contadores flutuantes provavelmente será a opção mais legível. E a legibilidade está próxima da correção na hierarquia dos requisitos profissionais.

Kilian Foth
fonte
48
Eu gosto de "um defeito esperando para acontecer". Claro, pode funcionar agora , mas uma brisa leve de alguém que passa por lá vai quebrá-lo.
precisa saber é o seguinte
10
Por exemplo, suponha que os requisitos mudem para que, em vez de 11 botões igualmente espaçados de 0 a 5 com a "linha padrão" no 4º botão, você tenha 16 botões igualmente espaçados de 0 a 5 com a "linha padrão" no 6º botão. Portanto, quem herdou esse código muda de 0,5 para 1,0 / 3,0 e de 1,5 para 5,0 / 3,0. O que acontece depois?
David K
8
Sim, estou desconfortável com a ideia de que alterar o que parece ser um número arbitrário (por mais "normal" que um número possa ser)) para outro número arbitrário (que parece igualmente "normal") realmente introduz um defeito.
Alexander - Restabelecer Monica
7
@ Alexander: certo, você precisaria de um comentário que disse DIFF must be an exactly-representable double that evenly divides STANDARD_LINE. Se você não quiser escrever esse comentário (e confiar em todos os futuros desenvolvedores para saber o suficiente sobre o ponto flutuante IEEE754 binary64 para entendê-lo), não escreva o código dessa maneira. ou seja, não escreva o código dessa maneira. Especialmente porque provavelmente não é ainda mais eficiente: a adição de FP possui uma latência mais alta que a adição de números inteiros e é uma dependência de loop. Além disso, os compiladores (mesmo os compiladores JIT?) Provavelmente se saem melhor ao criar loops com contadores inteiros.
Pedro Cordes
39

Como regra geral, os loops devem ser escritos de maneira a pensar em fazer algo n vezes. Se você estiver usando índices de ponto flutuante, não se trata mais de fazer algo n vezes, mas de executar até que uma condição seja atendida. Se essa condição for muito semelhante à i<nesperada por muitos programadores, o código parece estar fazendo uma coisa quando na verdade está fazendo outra, o que pode ser facilmente mal interpretado pelos programadores que estão analisando o código.

É um pouco subjetivo, mas, na minha humilde opinião, se você pode reescrever um loop para usar um índice inteiro para repetir um número fixo de vezes, deve fazê-lo. Portanto, considere a seguinte alternativa:

var DIFF=0.5;                           // pixel increment
var MAX=Math.floor(5.0/DIFF);           // 5.0 is max pixel width
var STANDARD_LINE=Math.floor(1.5/DIFF); // 1.5 is pixel width

for(var i=0;i<=MAX;i++){
    button.text=(i*DIFF)+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

O loop funciona em termos de números inteiros. Nesse caso, ié um número inteiro e também STANDARD_LINEé coagido a um número inteiro. Evidentemente, isso mudaria a posição de sua linha padrão se houvesse arredondamento e o mesmo acontece MAX, portanto, você deve se esforçar para evitar o arredondamento para obter uma renderização precisa. No entanto, você ainda tem a vantagem de alterar os parâmetros em termos de pixels e não números inteiros sem precisar se preocupar com a comparação de pontos flutuantes.

Neil
fonte
3
Você também pode considerar o arredondamento em vez de o revestimento nas atribuições, dependendo do que você deseja. Se a divisão deve fornecer um resultado inteiro, o piso pode causar surpresas se você encontrar números em que a divisão está ligeiramente desligada.
Ilkkachu
1
@ilkkachu True. Eu pensava que, se você estiver configurando 5.0 como a quantidade máxima de pixels, então, por meio de arredondamentos, você prefere estar no lado inferior desse 5.0 em vez de um pouco mais. 5.0 seria efetivamente o máximo. Embora o arredondamento possa ser preferível de acordo com o que você precisa fazer. Em ambos os casos, faz pouca diferença se a divisão criar um número inteiro de qualquer maneira.
Neil
4
Eu discordo fortemente. A melhor maneira de interromper um loop é pela condição que mais naturalmente expressa a lógica de negócios. Se a lógica de negócios é que você precisa de 11 botões, o loop deve parar na iteração 11. Se a lógica de negócios é que os botões estão separados por 0,5 até que a linha esteja cheia, o loop deve parar quando a linha estiver cheia. Há outras considerações que podem levar a escolha a um mecanismo ou outro, mas, na ausência dessas considerações, escolha o mecanismo que melhor corresponda aos requisitos de negócios.
Reintegrar Monica
Sua explicação seria completamente correta para Java / C ++ / Ruby / Python / ... Mas o Javascript não tem números inteiros, portanto, ie STANDARD_LINEapenas se parecem com números inteiros. Não há coerção, e DIFF, MAXe STANDARD_LINEsão todos apenas Numbers. Numbers usados ​​como números inteiros devem ser seguros abaixo 2**53, ainda são números de ponto flutuante.
Eric Duminil
@EricDuminil Sim, mas essa é a metade. A outra metade é legibilidade. Eu o menciono como a principal razão para fazê-lo dessa maneira, não para otimização.
Neil
20

Concordo com todas as outras respostas de que o uso de uma variável de loop não inteiro geralmente é um estilo ruim, mesmo em casos como este em que funcionará corretamente. Mas parece-me que há outra razão pela qual é um estilo ruim aqui.

Seu código "sabe" que as larguras de linha disponíveis são precisamente os múltiplos de 0,5, de 0 a 5,0. Deveria? Parece que é uma decisão da interface do usuário que pode mudar facilmente (por exemplo, talvez você queira que as lacunas entre as larguras disponíveis sejam maiores do que as larguras. 0,25, 0,5, 0,75, 1,0, 1,5, 2,0, 2,5, 3,0, 4,0, 5.0 ou algo assim).

Seu código "sabe" que todas as larguras de linhas disponíveis têm representações "agradáveis", tanto como números de ponto flutuante quanto como decimais. Isso também parece algo que pode mudar. (Você pode querer 0,1, 0,2, 0,3, ... em algum momento.)

Seu código "sabe" que o texto para colocar nos botões é simplesmente o que o Javascript transforma esses valores de ponto flutuante. Isso também parece algo que pode mudar. (Por exemplo, talvez um dia você queira larguras como 1/3, que provavelmente não deseja exibir como 0,33333333333333 ou qualquer outra coisa. Ou talvez queira ver "1,0" em vez de "1" para consistência com "1,5" .)

Tudo isso me parece manifestações de uma única fraqueza, que é uma espécie de mistura de camadas. Esses números de ponto flutuante fazem parte da lógica interna do software. O texto mostrado nos botões faz parte da interface do usuário. Eles devem estar mais separados do que no código aqui. Noções como "qual desses é o padrão que deve ser destacado?" são assuntos da interface do usuário e provavelmente não devem estar vinculados a esses valores de ponto flutuante. E seu loop aqui é realmente (ou pelo menos deveria ser) um loop sobre botões , não sobre larguras de linha . Assim, a tentação de usar uma variável de loop que aceita valores não inteiros desaparece: você usaria números inteiros sucessivos ou um loop for ... in / for ... do loop.

Meu sentimento é que a maioria dos casos em que se pode tentar repetir números não inteiros é assim: existem outras razões, totalmente não relacionadas a questões numéricas, pelas quais o código deve ser organizado de maneira diferente. (Nem todos os casos; posso imaginar que alguns algoritmos matemáticos podem ser expressos com mais precisão em termos de um loop sobre valores não inteiros.)

Gareth McCaughan
fonte
8

Um cheiro de código está usando flutuadores em loop assim.

O looping pode ser feito de várias maneiras, mas em 99,9% dos casos, você deve manter um incremento de 1 ou definitivamente haverá confusão, não apenas pelos desenvolvedores juniores.

Pieter B
fonte
Discordo, acho que múltiplos inteiros de 1 não são confusos em um loop for. Eu não consideraria isso um cheiro de código. Apenas frações.
CodeMonkey 02/02
3

Sim, você deseja evitar isso.

Os números de ponto flutuante são uma das maiores armadilhas para o programador desavisado (o que significa, na minha experiência, quase todo mundo). Dependendo dos testes de igualdade de ponto flutuante, até representar o dinheiro como ponto flutuante, é tudo uma grande confusão. A soma de um flutuador sobre o outro é um dos maiores infratores. Existem volumes inteiros de literatura científica sobre coisas assim.

Use números de ponto flutuante exatamente nos locais onde eles são apropriados, por exemplo, ao fazer cálculos matemáticos reais onde você precisar deles (como trigonometria, gráficos de funções de plotagem etc.) e seja super cuidadoso ao realizar operações seriais. A igualdade está certa. O conhecimento sobre qual conjunto específico de números é exato pelos padrões IEEE é muito misterioso e eu nunca dependeria disso.

No seu caso, não vai , pela Lei Murphys, vem o ponto em que a gestão quer que você não tem 0.0, 0.5, 1.0 ... mas 0,0, 0,4, 0,8 ... ou o que quer; você será enviado imediatamente, e seu programador júnior (ou você mesmo) irá depurar por muito tempo até encontrar o problema.

No seu código específico, eu realmente teria uma variável de loop inteiro. Representa o ibotão th, não o número em execução.

E eu provavelmente, por uma questão de clareza extra, não escreveria, i/2mas o i*0.5que deixaria bem claro o que está acontecendo.

var BUTTONS=11;
var STANDARD_LINE=3;

for(var i=0; i<BUTTONS; i++) {
    button.text = (i*0.5)+'';
    if (i==STANDARD_LINE) {
      button.color='red';
    }
}

Nota: conforme indicado nos comentários, o JavaScript não possui realmente um tipo separado para números inteiros. Mas números inteiros de até 15 dígitos são garantidos como precisos / seguros (consulte https://www.ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer ), portanto, para argumentos como este ("é isso mais confuso / propenso a erros de trabalhar com números inteiros ou não com números inteiros "), isso é apropriado para ter um tipo separado" em espírito "; no uso diário (loops, coordenadas de tela, índices de matriz etc.), não haverá surpresas com números inteiros representados Numbercomo JavaScript.

AnoE
fonte
Eu mudaria o nome BUTTONS para outra coisa - afinal, existem 11 botões e não 10. Talvez FIRST_BUTTON = 0, LAST_BUTTON = 10, STANDARD_LINE_BUTTON = 3. Além disso, sim, é assim que você deve fazê-lo.
precisa saber é o seguinte
É verdade, @EricDuminil, e adicionei um pouco sobre isso à resposta. Obrigado!
AnoE
1

Não acho que nenhuma das suas sugestões seja boa. Em vez disso, eu introduziria uma variável para o número de botões com base no valor máximo e no espaçamento. Então, é simples o suficiente para percorrer os índices do botão em si.

function precisionRound(number, precision) {
  let factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}

var maxButtonValue = 5.0;
var buttonSpacing = 0.5;

let countEstimate = precisionRound(maxButtonValue / buttonSpacing, 5);
var buttonCount = Math.floor(countEstimate) + 1;

var highlightPosition = 3;
var highlightColor = 'red';

for (let i=0; i < buttonCount; i++) {
    let buttonValue = i / buttonSpacing;
    button.text = buttonValue.toString();
    if (i == highlightPosition) {
        button.color = highlightColor;
    }
}

Pode ser mais código, mas também é mais legível e mais robusto.

Jared Goguen
fonte
0

Você pode evitar tudo calculando o valor que está mostrando, em vez de usar o contador de loop como o valor:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0; (i*DIFF) < MAX ; i=i+1){
    var val = i * DIFF

    button.text=val+'';

    if(val==STANDARD_LINE){
      button.color='red';
    }
}
Arnab Datta
fonte
-1

A aritmética de ponto flutuante é lenta e a aritmética de número inteiro é rápida; portanto, quando eu uso o ponto flutuante, não o utilizaria desnecessariamente onde números inteiros podem ser usados. É útil pensar sempre em números de ponto flutuante, mesmo constantes, como aproximados, com algum pequeno erro. É muito útil durante a depuração substituir números de ponto flutuante nativos por objetos de ponto flutuante mais / menos, onde você trata cada número como um intervalo em vez de um ponto. Dessa forma, você descobre imprecisões crescentes progressivas após cada operação aritmética. Portanto, "1.5" deve ser considerado "algum número entre 1,45 e 1,55" e "1,50" deve ser considerado "algum número entre 1,495 e 1,505".

Jacquez
fonte
5
A diferença de desempenho entre números inteiros e flutuantes é importante ao escrever o código C para um pequeno microprocessador, mas as modernas CPUs derivadas de x86 fazem ponto flutuante tão rápido que qualquer penalidade é facilmente eclipsada pela sobrecarga do uso de uma linguagem dinâmica. Em particular, o Javascript realmente não representa todos os números como ponto flutuante, usando a carga útil do NaN quando necessário?
usar o seguinte comando
1
"A aritmética de ponto flutuante é lenta e a aritmética inteira é rápida" é um truísmo histórico que você não deve reter à medida que o evangelho avança. Para adicionar ao que @leftaroundabout disse, não é apenas verdade que a penalidade seria quase irrelevante; é bem possível que as operações de ponto flutuante sejam mais rápidas que as operações inteiras equivalentes, graças à magia de compiladores de autovetorização e conjuntos de instruções que podem triturar grandes quantidades de carros alegóricos em um ciclo. Para esta pergunta, isso não é relevante, mas a suposição básica "número inteiro é mais rápido que float" não é verdadeira há um bom tempo.
Jeroen Mostert
1
@JeroenMostert SSE / AVX vetorizou operações para números inteiros e flutuantes, e você pode usar números inteiros menores (porque nenhum bit é desperdiçado no expoente), portanto, em princípio, muitas vezes ainda é possível obter mais desempenho do código inteiro altamente otimizado do que com carros alegóricos. Mas, novamente, isso não é relevante para a maioria dos aplicativos e definitivamente não é para esta pergunta.
usar o seguinte comando
1
@leftaroundabout: Claro. Meu argumento não era sobre qual é definitivamente mais rápido em qualquer situação, apenas que "eu sei que FP é lento e inteiro é rápido, então usarei números inteiros se possível" não é uma boa motivação antes mesmo de você enfrentar o problema. pergunta se o que você está fazendo precisa de otimização.
Jeroen Mostert