Não, essa não é outra pergunta "Por que (1 / 3,0) * 3! = 1" .
Ultimamente tenho lido sobre pontos flutuantes; especificamente, como o mesmo cálculo pode fornecer resultados diferentes em diferentes arquiteturas ou configurações de otimização.
Esse é um problema para jogos de vídeo que armazenam replays ou são rede ponto a ponto (em oposição ao servidor-cliente), que dependem de todos os clientes que geram exatamente os mesmos resultados sempre que executam o programa - uma pequena discrepância em um o cálculo de ponto flutuante pode levar a um estado de jogo drasticamente diferente em máquinas diferentes (ou mesmo na mesma máquina! )
Isso acontece mesmo entre os processadores que "seguem" o IEEE-754 , principalmente porque alguns processadores (ou seja, x86) usam precisão estendida dupla . Ou seja, eles usam registradores de 80 bits para fazer todos os cálculos e, em seguida, truncam para 64 ou 32 bits, levando a diferentes resultados de arredondamento do que máquinas que usam 64 ou 32 bits para os cálculos.
Eu já vi várias soluções para esse problema online, mas todas para C ++, não para C #:
- Desative o modo de precisão estendida dupla (para que todos os
double
cálculos usem IEEE-754 de 64 bits) usando_controlfp_s
(Windows),_FPU_SETCW
(Linux?) Oufpsetprec
(BSD). - Sempre execute o mesmo compilador com as mesmas configurações de otimização e exija que todos os usuários tenham a mesma arquitetura de CPU (sem reprodução entre plataformas). Como meu "compilador" é realmente o JIT, que pode otimizar diferentemente toda vez que o programa é executado , não acho que isso seja possível.
- Use aritmética de ponto fixo e evite
float
-odouble
completamente.decimal
funcionaria para esse fim, mas seria muito mais lento, e nenhuma dasSystem.Math
funções da biblioteca o suporta.
Então, isso é mesmo um problema em c #? E se eu pretender apenas suportar o Windows (não o Mono)?
Se for, existe alguma maneira de forçar meu programa a executar com precisão dupla normal?
Caso contrário, existem bibliotecas que ajudariam a manter os cálculos de ponto flutuante consistentes?
strictfp
palavra - chave, o que força todos os cálculos a serem feitos no tamanho indicado (float
oudouble
), em vez de um tamanho estendido. No entanto, o Java ainda tem muitos problemas com o suporte ao IEE-754. Muito (muito, muito) poucas linguagens de programação suportam bem o IEE-754.Respostas:
Não conheço nenhuma maneira de tornar determinantes os pontos flutuantes normais em .net. É permitido ao JITter criar código que se comporte de maneira diferente em plataformas diferentes (ou entre versões diferentes do .net). Portanto, o uso de
float
s normais no código .net determinístico não é possível.As soluções alternativas que considerei:
Acabei de iniciar uma implementação de software de matemática de ponto flutuante de 32 bits. Ele pode fazer cerca de 70 milhões de adições / multiplicações por segundo no meu i3 de 2,66 GHz. https://github.com/CodesInChaos/SoftFloat . Obviamente, ainda é muito incompleto e com erros.
fonte
decimal
primeiro, pois é muito mais simples. Somente se for muito lento para a tarefa em questão vale a pena pensar em outras abordagens.A especificação C # (§4.1.6 Tipos de ponto flutuante) permite especificamente que os cálculos de ponto flutuante sejam feitos usando precisão maior que a do resultado. Então, não, acho que você não pode fazer esses cálculos determinísticos diretamente no .Net. Outros sugeriram várias soluções alternativas, para que você pudesse experimentá-las.
fonte
double
cada vez que uma operação não remove os bits indesejados, produzindo resultados consistentes?A página a seguir pode ser útil no caso em que você precisa de portabilidade absoluta de tais operações. Ele discute o software para testar implementações do padrão IEEE 754, incluindo o software para emular operações de ponto flutuante. A maioria das informações provavelmente é específica para C ou C ++, no entanto.
http://www.math.utah.edu/~beebe/software/ieee/
Uma nota sobre ponto fixo
Os números binários de pontos fixos também podem funcionar bem como substitutos do ponto flutuante, como é evidente nas quatro operações aritméticas básicas:
Os números binários de pontos fixos podem ser implementados em qualquer tipo de dados inteiro, como int, long e BigInteger, e nos tipos não compatíveis com CLS uint e ulong.
Conforme sugerido em outra resposta, você pode usar as tabelas de pesquisa, em que cada elemento da tabela é um número de ponto fixo binário, para ajudar a implementar funções complexas, como seno, cosseno, raiz quadrada e assim por diante. Se a tabela de pesquisa for menos granular que o número do ponto fixo, é recomendável arredondar a entrada adicionando metade da granularidade da tabela de pesquisa à entrada:
fonte
const
vez destatic
constantes para que o compilador possa otimizá-las; prefira funções membro a funções estáticas (para que possamos chamar, ex. emmyDouble.LeadingZeros()
vez deIntDouble.LeadingZeros(myDouble)
); tentar evitar os nomes de variáveis de uma única letra (MultiplyAnyLength
, por exemplo, tem 9, o que torna muito difícil de seguir)unchecked
tipos não compatíveis com CLS comoulong
,uint
etc. para fins de velocidade - porque são tão raramente usados, o JIT não os otimiza de maneira agressiva, portanto, usá-los pode ser realmente mais lento do que usar tipos normais comolong
eint
. Além disso, o C # possui sobrecarga de operador , da qual esse projeto se beneficiaria bastante. Finalmente, existem testes de unidade associados? Além dessas pequenas coisas, trabalho incrível Peter, isso é ridiculamente impressionante!strictfp
.Isso é um problema para c #?
Sim. Arquiteturas diferentes são a menor das suas preocupações, taxas de quadros diferentes etc. podem levar a desvios devido a imprecisões nas representações de flutuação - mesmo que sejam as mesmas imprecisões (por exemplo, mesma arquitetura, exceto uma GPU mais lenta em uma máquina).
Posso usar System.Decimal?
Não há razão para que você não possa, no entanto, é um cachorro lento.
Existe uma maneira de forçar meu programa a executar em dupla precisão?
Sim. Hospede o tempo de execução do CLR você mesmo ; e compile todas as chamadas / sinalizações necessárias (que alteram o comportamento da aritmética de ponto flutuante) no aplicativo C ++ antes de chamar CorBindToRuntimeEx.
Existem bibliotecas que ajudariam a manter os cálculos de ponto flutuante consistentes?
Não que eu saiba.
Existe outra maneira de resolver isso?
Já lidei com esse problema antes, a idéia é usar QNumbers . Eles são uma forma de reais com ponto fixo; mas não ponto fixo na base 10 (decimal) - em vez da base 2 (binária); por causa disso, as primitivas matemáticas nelas (add, sub, mul, div) são muito mais rápidas que os ingênuos pontos fixos da base 10; especialmente se
n
for o mesmo para os dois valores (o que no seu caso seria). Além disso, por serem integrais, têm resultados bem definidos em todas as plataformas.Lembre-se de que a taxa de quadros ainda pode afetá-los, mas não é tão ruim e é facilmente corrigida usando pontos de sincronização.
Posso usar mais funções matemáticas com QNumbers?
Sim, faça uma ida e volta decimal para fazer isso. Além disso, você realmente deve usar tabelas de pesquisa para as funções trig (sin, cos); como eles podem realmente dar resultados diferentes em plataformas diferentes - e se você os codificar corretamente, eles podem usar o QNumbers diretamente.
fonte
De acordo com essa entrada do blog do MSDN, um pouco antiga, o JIT não usará SSE / SSE2 para ponto flutuante, é tudo x87. Por isso, como você mencionou, você precisa se preocupar com modos e sinalizadores, e no C # isso não é possível de controlar. Portanto, o uso de operações normais de ponto flutuante não garantirá exatamente o mesmo resultado em todas as máquinas do seu programa.
Para obter uma reprodutibilidade precisa da precisão dupla, você precisará fazer a emulação de ponto flutuante (ou ponto fixo) do software. Não conheço bibliotecas C # para fazer isso.
Dependendo das operações necessárias, você poderá escapar com precisão única. Aqui está a ideia:
O grande problema do x87 é que os cálculos podem ser feitos com precisão de 53 ou 64 bits, dependendo do sinalizador de precisão e se o registro foi derramado na memória. Porém, para muitas operações, executar a operação com alta precisão e arredondar para menor precisão garantirá a resposta correta, o que implica que a resposta será a mesma em todos os sistemas. Se você obtém precisão extra, não importa, pois você tem precisão suficiente para garantir a resposta certa em ambos os casos.
Operações que devem funcionar neste esquema: adição, subtração, multiplicação, divisão, sqrt. Coisas como pecado, exp, etc. não funcionam (os resultados geralmente correspondem, mas não há garantia). "Quando o arredondamento duplo é inócuo?" Referência do ACM (pagamento pago reg.)
Espero que isto ajude!
fonte
Como já foi dito por outras respostas: Sim, esse é um problema no C # - mesmo quando o Windows é puro.
Quanto a uma solução: você pode reduzir (e com algum esforço / desempenho) evitar completamente o problema se usar a
BigInteger
classe interna e escalar todos os cálculos para uma precisão definida usando um denominador comum para qualquer cálculo / armazenamento desses números.Conforme solicitado pelo OP - em relação ao desempenho:
System.Decimal
representa número com 1 bit para um sinal e número inteiro de 96 bits e uma "escala" (representando onde está o ponto decimal). Para todos os cálculos que você faz, ele deve operar nessa estrutura de dados e não pode usar nenhuma instrução de ponto flutuante incorporada à CPU.A
BigInteger
"solução" faz algo semelhante - apenas que você pode definir quantos dígitos precisa / deseja ... talvez você queira apenas 80 bits ou 240 bits de precisão.A lentidão vem sempre da necessidade de simular todas as operações nesse número por meio de instruções somente inteiras sem usar as instruções internas da CPU / FPU, que por sua vez levam a muito mais instruções por operação matemática.
Para diminuir o impacto no desempenho, existem várias estratégias - como QNumbers (veja a resposta de Jonathan Dickinson - a matemática de ponto flutuante é consistente em C #? Pode ser? ) E / ou cache (por exemplo, cálculos trigonométricos ...) etc.
fonte
BigInteger
está disponível apenas no .Net 4.0.BigInteger
excede até o desempenho do Decimal.Decimal
(@ Jonathan Dickinson - 'dog slow') ouBigInteger
(comentário do @CodeInChaos acima) - alguém pode fornecer uma pequena explicação sobre esses hits do desempenho e se / por que eles são realmente uma barreira para fornecer uma solução.Bem, aqui seria a minha primeira tentativa de como fazer isso :
(Eu acredito que você pode simplesmente compilar em uma DLL de 32 bits e usá-la com x86 ou AnyCpu [ou provavelmente apenas com o x86 em um sistema de 64 bits; veja o comentário abaixo]).
Então, supondo que funcione, se você quiser usar o Mono, imagino que você possa replicar a biblioteca em outras plataformas x86 de maneira semelhante (não COM, é claro; embora, talvez, com vinho? Um pouco fora da minha área uma vez nós vamos lá embora ...).
Supondo que você possa fazê-lo funcionar, você deve poder configurar funções personalizadas que podem executar várias operações ao mesmo tempo para corrigir quaisquer problemas de desempenho, e você terá uma matemática de ponto flutuante que permite obter resultados consistentes em plataformas com uma quantidade mínima do código escrito em C ++ e deixando o restante do código em C #.
fonte
x86
poderá carregar a dll de 32 bits.Não sou desenvolvedor de jogos, embora tenha muita experiência com problemas computacionalmente difíceis ... então, farei o meu melhor.
A estratégia que eu adotaria é essencialmente esta:
A curta duração disso é: você precisa encontrar um equilíbrio. Se você gasta 30ms de renderização (~ 33fps) e apenas 1ms faz a detecção de colisão (ou insere alguma outra operação altamente sensível) - mesmo se você triplicar o tempo necessário para fazer a aritmética crítica, o impacto que ela tem na taxa de quadros é você cai de 33,3fps para 30,3fps.
Sugiro que você analise tudo, explique quanto tempo é gasto em cada um dos cálculos visivelmente caros, repita as medições com 1 ou mais métodos para resolver esse problema e veja qual é o impacto.
fonte
A verificação dos links nas outras respostas deixa claro que você nunca terá garantia de que o ponto flutuante seja "corretamente" implementado ou se receberá sempre uma certa precisão para um determinado cálculo, mas talvez você possa fazer um melhor esforço (1) truncando todos os cálculos para um mínimo comum (por exemplo, se diferentes implementações fornecerem precisão de 32 a 80 bits, sempre truncando todas as operações para 30 ou 31 bits), (2) tenha uma tabela de alguns casos de teste na inicialização (casos limítrofes de adicionar, subtrair, multiplicar, dividir, sqrt, cosseno etc.) e se a implementação calcular valores correspondentes à tabela, não se preocupe em fazer os ajustes.
fonte
float
tipo de dados faz nas máquinas x86 - no entanto, isso causa resultados ligeiramente diferentes das máquinas que fazem todos os seus cálculos usando apenas 32 bits e essas pequenas alterações se propagam ao longo do tempo. Portanto, a questão.Sua pergunta é bastante difícil e técnica O_o. No entanto, posso ter uma ideia.
Você com certeza sabe que a CPU faz alguns ajustes após qualquer operação flutuante. E a CPU oferece várias instruções diferentes que fazem diferentes operações de arredondamento.
Portanto, para uma expressão, seu compilador escolherá um conjunto de instruções que o levará a um resultado. Mas qualquer outro fluxo de trabalho de instrução, mesmo que pretenda calcular a mesma expressão, pode fornecer outro resultado.
Os 'erros' cometidos por um ajuste de arredondamento aumentam a cada instrução adicional.
Como exemplo, podemos dizer que, no nível da montagem: a * b * c não é equivalente a a * c * b.
Não tenho muita certeza disso, você precisará pedir a alguém que conheça a arquitetura da CPU muito mais do que eu: p
No entanto, para responder à sua pergunta: em C ou C ++, você pode resolver o seu problema porque possui algum controle sobre o código da máquina gerado pelo seu compilador, mas no .NET você não tem nenhum. Portanto, desde que o código da sua máquina possa ser diferente, você nunca terá certeza do resultado exato.
Estou curioso para saber de que maneira isso pode ser um problema, pois a variação parece muito mínima, mas se você precisar de uma operação realmente precisa, a única solução em que posso pensar será aumentar o tamanho dos seus registros flutuantes. Use precisão dupla ou mesmo longa dupla, se puder (não tenho certeza se isso é possível usando a CLI).
Espero ter sido claro o suficiente, não sou perfeito em inglês (... de todo: s)
fonte