O mal-entendido da aritmética de ponto flutuante e suas falhas é uma das principais causas de surpresa e confusão na programação (considere o número de perguntas no Stack Overflow referentes a "números que não são adicionados corretamente"). Considerando que muitos programadores ainda não entenderam suas implicações, ele tem o potencial de introduzir muitos erros sutis (especialmente no software financeiro). O que pode linguagens de programação fazer para evitar suas armadilhas para aqueles que não estão familiarizados com os conceitos, enquanto continua a oferecer a sua velocidade quando a precisão não é crítica para aqueles que fazem compreender os conceitos?
language-design
Adam Paynter
fonte
fonte
Respostas:
Você diz "especialmente para software financeiro", o que traz à tona um dos meus ódios de estimação: dinheiro não é uma bóia, é um int .
Claro, parece um carro alegórico. Tem um ponto decimal lá. Mas isso é apenas porque você está acostumado a unidades que confundem o problema. O dinheiro sempre vem em quantidades inteiras. Na América, é centavos. (Em certos contextos, acho que podem ser moinhos , mas ignore isso por enquanto.)
Então, quando você diz US $ 1,23, são 123 centavos. Sempre, sempre, sempre faça suas contas nesses termos, e você ficará bem. Para mais informações, veja:
Respondendo à pergunta diretamente, as linguagens de programação devem incluir apenas um tipo Money como uma primitiva razoável.
atualizar
Ok, eu deveria ter dito "sempre" duas vezes, em vez de três. O dinheiro é realmente sempre um int; quem pensa de outra forma é bem-vindo a tentar me enviar 0,3 centavos e me mostrar o resultado em seu extrato bancário. Mas, como os comentaristas apontam, há raras exceções quando você precisa fazer matemática de ponto flutuante em números do tipo dinheiro. Por exemplo, certos tipos de preços ou cálculos de juros. Mesmo assim, esses devem ser tratados como exceções. O dinheiro entra e sai como quantidades inteiras; portanto, quanto mais próximo o sistema estiver disso, mais saudável será.
fonte
Decimal
é o único sistema sensato para lidar com isso, e seu comentário "ignora isso por enquanto" é o prenúncio de desgraça para programadores em todos os lugares: PFornecer suporte para um tipo decimal ajuda em muitos casos. Muitos idiomas têm um tipo decimal, mas são subutilizados.
É importante compreender a aproximação que ocorre quando se trabalha com representação de números reais. Usar os tipos de ponto decimal e de ponto flutuante
9 * (1/9) != 1
é uma afirmação correta. Quando constantes, um otimizador pode otimizar o cálculo para que esteja correto.Fornecer um operador aproximado ajudaria. No entanto, essas comparações são problemáticas. Observe que 0,9999 trilhão de dólares é aproximadamente igual a 1 trilhão de dólares. Você poderia depositar a diferença na minha conta bancária?
fonte
0.9999...
trilhão de dólares é exatamente igual a 1 trilhão de dólares.0.99999...
. Todos eles truncam em algum momento, resultando em uma desigualdade.0.9999
é igual o suficiente para a engenharia. Para fins financeiros, não é.Fomos informados sobre o que fazer no primeiro ano (no segundo ano) da aula de ciências da computação quando eu fui para a universidade (este curso também era um pré-requisito para a maioria dos cursos de ciências)
Lembro-me do palestrante dizendo "Números de ponto flutuante são aproximações. Use tipos inteiros por dinheiro. Use FORTRAN ou outro idioma com números BCD para obter um cálculo preciso". (e depois apontou a aproximação, usando o exemplo clássico de 0,2 impossível de representar com precisão no ponto flutuante binário). Isso também apareceu naquela semana nos exercícios de laboratório.
A mesma palestra: "Se você precisar obter mais precisão do ponto flutuante, classifique seus termos. Adicione números pequenos, não grandes." Isso ficou na minha mente.
Alguns anos atrás, eu tinha uma geometria esférica que precisava ser muito precisa e ainda rápida. O dobro de 80 bits nos PCs não estava diminuindo, então eu adicionei alguns tipos ao programa que classificavam os termos antes de executar operações comutativas. Problema resolvido.
Antes de reclamar da qualidade do violão, aprenda a tocar.
Eu tinha um colega de trabalho há quatro anos que trabalhava na JPL. Ele expressou descrença de que usamos FORTRAN para algumas coisas. (Precisávamos de simulações numéricas super precisas calculadas off-line.) "Substituímos todo esse FORTRAN por C ++", disse ele, orgulhoso. Parei de me perguntar por que eles perderam um planeta.
fonte
1.0 + 0.1 + ... + 0.1
(repetido 10 vezes) retorna à1.0
medida que cada resultado intermediário é arredondado. Fazê-lo de outra maneira, você obtém resultados intermediários de0.2
,0.3
, ...,1.0
e, finalmente2.0
. Este é um exemplo extremo, mas com números de ponto flutuante realistas, problemas semelhantes acontecem. A idéia básica é que a adição de números semelhantes em tamanho leve ao menor erro. Comece com os números menores, pois sua soma é maior e, portanto, mais adequada para adição a números maiores.Não acredito que algo possa ou deva ser feito no nível do idioma.
fonte
Decimal
quando se trata de teste de igualdade. A diferença entre1.0m/7.0m*7.0m
e1.0m
pode ser muitas ordens de magnitude menor que a diferença entre1.0/7.0*7.0
, mas não é zero.Por padrão, os idiomas devem usar argumentos de precisão arbitrária para números não inteiros.
Quem precisa otimizar pode sempre pedir carros alegóricos. Usá-los como padrão fazia sentido em C e outras linguagens de programação de sistemas, mas não na maioria dos idiomas populares atualmente.
fonte
double
. Se um cálculo precisa ser preciso com uma parte por milhão, é melhor gastar um microssegundo calculando-o dentro de algumas partes por bilhão, do que gastar um segundo calculando-o com precisão absoluta.Os dois maiores problemas envolvendo números de ponto flutuante são:
O primeiro tipo de falha pode ser solucionado apenas fornecendo um tipo composto que inclui informações de valor e unidade. Por exemplo, um
length
ouarea
valor que incorpora a unidade (metros ou metros quadrados ou pés e pés quadrados, respectivamente). Caso contrário, você deve ser diligente em trabalhar sempre com um tipo de unidade de medida e somente converter em outro quando compartilharmos a resposta com um ser humano.O segundo tipo de falha é uma falha conceitual. As falhas se manifestam quando as pessoas as consideram como números absolutos . Isso afeta operações de igualdade, erros cumulativos de arredondamento etc. Por exemplo, pode ser correto que, para um sistema, duas medições sejam equivalentes dentro de uma certa margem de erro. Ou seja, .999 e 1.001 são aproximadamente os mesmos que 1.0 quando você não se importa com diferenças menores que +/- .1. No entanto, nem todos os sistemas são tão brandos.
Se houver alguma facilidade no nível do idioma, eu chamaria isso de precisão da igualdade . Nas estruturas de teste NUnit, JUnit e de construção semelhante, é possível controlar a precisão considerada correta. Por exemplo:
Se, por exemplo, C # ou Java foram alterados para incluir um operador de precisão, pode ser algo como isto:
No entanto, se você fornecer um recurso como esse, também deverá considerar o caso em que a igualdade é boa se os lados +/- não forem os mesmos. Por exemplo, + 1 / -10 consideraria dois números equivalentes se um deles estivesse dentro de 1 a mais ou 10 a menos que o primeiro número. Para lidar com esse caso, pode ser necessário adicionar uma
range
palavra - chave:fonte
O que as linguagens de programação podem fazer? Não sei se há uma resposta para essa pergunta, porque qualquer coisa que o compilador / intérprete faça em nome do programador para facilitar sua vida geralmente funciona contra desempenho, clareza e legibilidade. Eu acho que tanto a maneira C ++ (pague apenas pelo que você precisa) quanto a maneira Perl (princípio da menor surpresa) são válidas, mas depende da aplicação.
Os programadores ainda precisam trabalhar com a linguagem e entender como ela lida com pontos flutuantes, porque, se não o fizerem, farão suposições, e um dia o comportamento perscrutado não corresponderá às suposições.
Minha opinião sobre o que o programador precisa saber:
fonte
Use padrões sensíveis, por exemplo, suporte embutido para decimais.
O Groovy faz isso muito bem, embora com um pouco de esforço você ainda possa escrever código para introduzir a imprecisão de ponto flutuante.
fonte
Concordo que não há nada a fazer no nível do idioma. Os programadores devem entender que os computadores são discretos e limitados e que muitos dos conceitos matemáticos representados neles são apenas aproximações.
Não importa o ponto flutuante. É preciso entender que metade dos padrões de bits são usados para números negativos e que 2 ^ 64 é realmente muito pequeno para evitar problemas típicos com aritmética inteira.
fonte
x
==y
não implica que a execução de uma computação ativadax
produzirá o mesmo resultado que a execução da mesma computação ativaday
).Uma coisa que os idiomas podem fazer - remover a comparação de igualdade dos tipos de ponto flutuante que não seja uma comparação direta com os valores da NAN.
O teste de igualdade só existiria como uma chamada de função que levou os dois valores e um delta, ou para idiomas como C # que permitem que os tipos tenham métodos um EqualsTo que leva o outro valor e o delta.
fonte
Acho estranho que ninguém tenha apontado o truque numérico racional da família Lisp.
Sério, abra o sbcl e faça o seguinte:
(+ 1 3)
e você recebe 4. Se*( 3 2)
você obtém 6. Agora tente(/ 5 3)
e você obtém 5/3 ou 5 terços.Isso deve ajudar um pouco em algumas situações, não deveria?
fonte
Uma coisa que eu gostaria de ver seria um reconhecimento de que
double
afloat
deve ser considerado como uma conversão alargando, ao mesmo tempofloat
quedouble
está a diminuir (*). Isso pode parecer contra-intuitivo, mas considere o que os tipos realmente significam:Se alguém possui um
double
que detém a melhor representação da quantidade "um décimo" e a convertefloat
, o resultado será "13.421.773,5 / 134.217.728, mais ou menos 1 / 268.435.456 ou mais", que é uma descrição correta do valor.Por outro lado, se alguém possui um
float
que mantém a melhor representação da quantidade "um décimo" e a convertedouble
, o resultado será "13.421.773,5 / 134.217.728, mais ou menos 1 / 72.057.594.037.927.936 ou mais" - um nível de precisão implícita o que está errado por um fator de mais de 53 milhões.Embora o padrão IEEE-744 exija que a matemática de ponto flutuante seja executada como se todo número de ponto flutuante represente a quantidade numérica exata precisamente no centro de seu intervalo, isso não deve ser considerado como implicando que os valores de ponto flutuante realmente representem os valores exatos quantidades numéricas. Em vez disso, o requisito de que os valores sejam considerados no centro de seus intervalos decorre de três fatos: (1) os cálculos devem ser realizados como se os operandos tivessem alguns valores precisos específicos; (2) suposições consistentes e documentadas são mais úteis que as inconsistentes ou não documentadas; (3) se alguém fizer uma suposição consistente, nenhuma outra suposição consistente será melhor do que assumir que uma quantidade representa o centro de seu intervalo.
Aliás, lembro-me de cerca de 25 anos atrás, alguém inventou um pacote numérico para C que usava "tipos de intervalo", cada um consistindo em um par de flutuadores de 128 bits; todos os cálculos seriam feitos de maneira a calcular o valor mínimo e máximo possível para cada resultado. Se alguém executasse um grande cálculo iterativo longo e obtivesse um valor de [12.53401391134 12.53902812673], pode-se ter certeza de que, embora muitos dígitos de precisão tenham sido perdidos por erros de arredondamento, o resultado ainda pode ser razoavelmente expresso como 12,54 (e não foi '' realmente 12,9 ou 53,2). Estou surpreso por não ter visto nenhum suporte para esses tipos em nenhum idioma convencional, especialmente porque eles parecem se encaixar em unidades matemáticas que podem operar com vários valores em paralelo.
(*) Na prática, geralmente é útil usar valores de precisão dupla para armazenar cálculos intermediários ao trabalhar com números de precisão única; portanto, ter que usar uma conversão de tipo para todas essas operações pode ser irritante. Os idiomas poderiam ajudar por ter um tipo "duplo difuso", que executaria os cálculos em dobro e poderia ser convertido livremente para e do único; isso seria especialmente útil se as funções que recebem parâmetros de tipo
double
e retornodouble
pudessem ser marcadas para gerar automaticamente uma sobrecarga que aceita e retorna "fuzzy double".fonte
Se mais linguagens de programação tirassem uma página dos bancos de dados e permitissem aos desenvolvedores especificar o comprimento e a precisão de seus tipos de dados numéricos, eles poderiam reduzir substancialmente a probabilidade de erros relacionados a pontos flutuantes. Se um idioma permitisse que um desenvolvedor declarasse uma variável como Flutuante (2), indicando que eles precisavam de um número de ponto flutuante com dois dígitos decimais de precisão, ele poderia executar operações matemáticas com muito mais segurança. Se o fizesse representando a variável como um inteiro internamente e dividindo por 100 antes de expor o valor, poderia melhorar a velocidade usando os caminhos aritméticos inteiros mais rápidos. A semântica de um Float (2) também permitiria que os desenvolvedores evitassem a constante necessidade de arredondar os dados antes de enviá-los, uma vez que um Float (2) arredondaria os dados inerentemente para duas casas decimais.
Obviamente, você precisa permitir que um desenvolvedor solicite um valor de ponto flutuante de precisão máxima quando o desenvolvedor precisar dessa precisão. E você apresentaria problemas nos quais expressões ligeiramente diferentes da mesma operação matemática produzem resultados potencialmente diferentes devido às operações de arredondamento intermediário quando os desenvolvedores não possuem precisão suficiente em suas variáveis. Mas, pelo menos no mundo dos bancos de dados, isso não parece grande coisa. A maioria das pessoas não está fazendo os tipos de cálculos científicos que exigem muita precisão em resultados intermediários.
fonte
Float(2)
como você propõe não deve ser chamadoFloat
, pois não há nada flutuando aqui, certamente não o "ponto decimal".Os itens acima são aplicáveis em alguns casos, mas não são realmente uma solução geral para lidar com valores flutuantes. A solução real é entender o problema e aprender a lidar com ele. Se você estiver usando cálculos de ponto flutuante, sempre verifique se seus algoritmos são numericamente estáveis . Há um enorme campo de matemática / ciência da computação que se relaciona com o problema. Chama-se Análise Numérica .
fonte
Como outras respostas observaram, a única maneira real de evitar armadilhas de ponto flutuante no software financeiro é não usá-lo lá. Isso pode ser realmente viável - se você fornecer uma biblioteca bem projetada, dedicada à matemática financeira .
As funções projetadas para importar estimativas de ponto flutuante devem ser claramente rotuladas como tal e fornecidas com parâmetros apropriados para essa operação, por exemplo:
A única maneira real de evitar armadilhas de ponto flutuante em geral é a educação - os programadores precisam ler e entender algo como o que todo programador deve saber sobre aritmética de ponto flutuante .
Algumas coisas que podem ajudar, no entanto:
isNear()
função.fonte
Muitos programadores ficariam surpresos com o fato de o COBOL acertar ... na primeira versão do COBOL, não havia ponto flutuante, apenas decimal, e a tradição no COBOL continuou até hoje em que a primeira coisa que você pensa ao declarar um número é decimal. .. ponto flutuante só seria usado se você realmente precisasse. Quando C apareceu, por algum motivo, não havia um tipo decimal primitivo; portanto, na minha opinião, foi aí que todos os problemas começaram.
fonte