O código a seguir funciona no Visual Studio 2008 com e sem otimização. Mas só funciona no g ++ sem otimização (O0).
#include <cstdlib>
#include <iostream>
#include <cmath>
double round(double v, double digit)
{
double pow = std::pow(10.0, digit);
double t = v * pow;
//std::cout << "t:" << t << std::endl;
double r = std::floor(t + 0.5);
//std::cout << "r:" << r << std::endl;
return r / pow;
}
int main(int argc, char *argv[])
{
std::cout << round(4.45, 1) << std::endl;
std::cout << round(4.55, 1) << std::endl;
}
A saída deve ser:
4.5
4.6
Mas g ++ com otimização ( O1
- O3
) produzirá:
4.5
4.5
Se eu adicionar a volatile
palavra - chave antes de t, funciona, então pode haver algum tipo de bug de otimização?
Teste em g ++ 4.1.2 e 4.4.4.
Aqui está o resultado em ideone: http://ideone.com/Rz937
E a opção que testo no g ++ é simples:
g++ -O2 round.cpp
O resultado mais interessante, mesmo eu habilitando a /fp:fast
opção no Visual Studio 2008, o resultado ainda está correto.
Mais perguntas:
Eu estava me perguntando, devo sempre ativar essa -ffloat-store
opção?
Porque a versão g ++ que testei é fornecida com CentOS / Red Hat Linux 5 e CentOS / Redhat 6 .
Compilei muitos de meus programas nessas plataformas e estou preocupado que isso cause bugs inesperados em meus programas. Parece um pouco difícil investigar todo o meu código C ++ e bibliotecas usadas se eles têm esses problemas. Alguma sugestão?
Alguém está interessado em saber /fp:fast
por que o Visual Studio 2008 ainda funciona? Parece que o Visual Studio 2008 é mais confiável nesse problema do que o g ++?
fonte
Respostas:
Os processadores Intel x86 usam a precisão estendida de 80 bits internamente, ao passo que
double
normalmente é de 64 bits. Diferentes níveis de otimização afetam a frequência com que os valores de ponto flutuante da CPU são salvos na memória e, portanto, arredondados da precisão de 80 bits para a precisão de 64 bits.Use a
-ffloat-store
opção gcc para obter os mesmos resultados de ponto flutuante com diferentes níveis de otimização.Como alternativa, use o
long double
tipo, que normalmente tem 80 bits de largura no gcc para evitar o arredondamento da precisão de 80 bits para 64 bits.man gcc
diz tudo:Em compilações x86_64, os compiladores usam registros SSE para
float
edouble
por padrão, de forma que nenhuma precisão estendida seja usada e esse problema não ocorra.gcc
a opção do compilador-mfpmath
controla isso.fonte
inf
. Não existe uma boa regra prática, os testes de unidade podem fornecer uma resposta definitiva.Como Maxim Yegorushkin já observou em sua resposta, parte do problema é que internamente seu computador está usando uma representação de ponto flutuante de 80 bits. No entanto, isso é apenas parte do problema. A base do problema é que qualquer número na forma n.nn5 não tem uma representação binária flutuante exata. Esses casos extremos são sempre números inexatos.
Se você realmente deseja que seu arredondamento seja capaz de contornar de forma confiável esses casos extremos, você precisa de um algoritmo de arredondamento que aborde o fato de que n.n5, n.nn5 ou n.nnn5, etc. (mas não n.5) é sempre inexato. Encontre a caixa de canto que determina se algum valor de entrada é arredondado para cima ou para baixo e retorna o valor arredondado para cima ou para baixo com base em uma comparação com esta caixa de canto. E você precisa tomar cuidado para que um compilador otimizado não coloque aquele caso de canto encontrado em um registro de precisão estendido.
Consulte Como o Excel arredonda números flutuantes com êxito, embora sejam imprecisos? para tal algoritmo.
Ou você pode simplesmente conviver com o fato de que os casos mais difíceis às vezes terminam erroneamente.
fonte
Compiladores diferentes têm configurações de otimização diferentes. Algumas dessas configurações de otimização mais rápidas não mantêm regras estritas de ponto flutuante de acordo com IEEE 754 . Visual Studio tem uma configuração específica,
/fp:strict
,/fp:precise
,/fp:fast
, em que/fp:fast
viola o padrão sobre o que pode ser feito. Você pode descobrir que esse sinalizador é o que controla a otimização em tais configurações. Você também pode encontrar uma configuração semelhante no GCC que muda o comportamento.Se esse for o caso, a única coisa diferente entre os compiladores é que o GCC procuraria o comportamento de ponto flutuante mais rápido por padrão em otimizações mais altas, enquanto o Visual Studio não altera o comportamento de ponto flutuante com níveis de otimização mais altos. Portanto, pode não ser necessariamente um bug real, mas o comportamento intencional de uma opção que você não sabia que estava ativando.
fonte
-ffast-math
chave para o GCC que, e não é ativada por nenhum dos-O
níveis de otimização desde a citação: "pode resultar em saída incorreta para programas que dependem de uma implementação exata de regras / especificações IEEE ou ISO para funções matemáticas."-ffast-math
e algumas outras coisas no meug++ 4.4.3
e ainda não consigo reproduzir o problema.-ffast-math
eu obtenho4.5
em ambos os casos níveis de otimização maiores que0
.4.5
com-O1
e-O2
, mas não com-O0
e-O3
no GCC 4.4.3, mas com-O1,2,3
no GCC 4.6.1.)Isso significa que o problema está relacionado às instruções de depuração. E parece que há um erro de arredondamento causado pelo carregamento dos valores nos registradores durante as instruções de saída, e é por isso que outros descobriram que você pode corrigir isso com
-ffloat-store
Para ser leviano, deve haver uma razão que alguns programadores não ligar
-ffloat-store
, caso contrário, a opção não existiria (do mesmo modo, deve haver uma razão que alguns programadores não ligar-ffloat-store
). Eu não recomendaria sempre ligá-lo ou desligá-lo sempre. Ativá-lo impede algumas otimizações, mas desativá-lo permite o tipo de comportamento que você está obtendo.Mas, geralmente, há alguma incompatibilidade entre números de ponto flutuante binário (como o computador usa) e números de ponto flutuante decimal (que as pessoas estão familiarizadas), e essa incompatibilidade pode causar um comportamento semelhante ao que você está obtendo (para ser claro, o comportamento que você está recebendo não é causado por essa incompatibilidade, mas um comportamento semelhante pode ser). O fato é que, como você já tem alguma imprecisão ao lidar com ponto flutuante, não posso dizer que
-ffloat-store
isso torne isso melhor ou pior.Em vez disso, você pode querer procurar outras soluções para o problema que está tentando resolver (infelizmente, Koenig não aponta para o papel real, e eu realmente não consigo encontrar um lugar "canônico" óbvio para ele, então eu terá que enviar você para o Google ).
Se você não estiver arredondando para fins de saída, provavelmente examinaria
std::modf()
(incmath
) estd::numeric_limits<double>::epsilon()
(inlimits
). Pensando naround()
função original , acredito que seria mais limpo substituir a chamada destd::floor(d + .5)
por uma chamada para esta função:Acho que isso sugere a seguinte melhoria:
Uma nota simples:
std::numeric_limits<T>::epsilon()
é definida como "o menor número adicionado a 1 que cria um número diferente de 1." Você geralmente precisa usar um épsilon relativo (ou seja, dimensionar épsilon de alguma forma para explicar o fato de que você está trabalhando com números diferentes de "1"). A soma ded
,.5
estd::numeric_limits<double>::epsilon()
deve estar perto de 1, então o agrupamento de meios de adição questd::numeric_limits<double>::epsilon()
será sobre o tamanho certo para o que estamos fazendo. Nostd::numeric_limits<double>::epsilon()
mínimo , será muito grande (quando a soma de todos os três for menor que um) e pode nos fazer arredondar alguns números para cima, quando não deveríamos.Hoje em dia, você deve considerar
std::nearbyint()
.fonte
x - nextafter(x, INFINITY)
está relacionado a 1 ulp para x (mas não use isso; tenho certeza de que há casos esquivos e acabei de inventar isso). O exemplo cppreference paraepsilon()
tem um exemplo de escalonamento para obter um erro relativo baseado em ULP .-ffloat-store
é: não use x87 em primeiro lugar. Use matemática SSE2 (binários de 64 bits ou-mfpmath=sse -msse2
para criar binários de 32 bits antiquados), porque SSE / SSE2 tem temporários sem precisão extra.double
efloat
vars em registros XMM estão realmente no formato IEEE de 64 bits ou 32 bits. (Ao contrário do x87, em que os registros são sempre de 80 bits e o armazenamento na memória é arredondado para 32 ou 64 bits.)A resposta aceita está correta se você estiver compilando para um destino x86 que não inclui SSE2. Todos os processadores x86 modernos suportam SSE2, portanto, se você pode tirar proveito disso, você deve:
Vamos decompô-lo.
-mfpmath=sse -msse2
. Isso executa o arredondamento usando registradores SSE2, que é muito mais rápido do que armazenar todos os resultados intermediários na memória. Observe que este já é o padrão no GCC para x86-64. Do wiki do GCC :-ffp-contract=off
. No entanto, controlar o arredondamento não é suficiente para uma correspondência exata. As instruções FMA (fusão, multiplicação e adição) podem alterar o comportamento de arredondamento em comparação com suas contrapartes não fundidas, portanto, precisamos desativá-lo. Este é o padrão no Clang, não no GCC. Conforme explicado por esta resposta :Ao desabilitar o FMA, obtemos resultados que correspondem exatamente na depuração e liberação, ao custo de algum desempenho (e precisão). Ainda podemos aproveitar outras vantagens de desempenho de SSE e AVX.
fonte
Eu cavei mais neste problema e posso trazer mais precisões. Primeiro, as representações exatas de 4,45 e 4,55 de acordo com gcc em x84_64 são as seguintes (com libquadmath para imprimir a última precisão):
Como Maxim disse acima, o problema é devido ao tamanho de 80 bits dos registradores da FPU.
Mas por que o problema nunca ocorre no Windows? no IA-32, o x87 FPU foi configurado para usar uma precisão interna para a mantissa de 53 bits (equivalente a um tamanho total de 64 bits:)
double
. Para Linux e Mac OS, a precisão padrão de 64 bits foi usada (equivalente a um tamanho total de 80 bits:)long double
. Portanto, o problema deveria ser possível, ou não, nessas diferentes plataformas, alterando a palavra de controle da FPU (assumindo que a sequência de instruções acionaria o bug). O problema foi relatado ao gcc como bug 323 (leia pelo menos o comentário 92!).Para mostrar a precisão da mantissa no Windows, você pode compilar em 32 bits com VC ++:
e no Linux / Cygwin:
Observe que com o gcc você pode definir a precisão da FPU com
-mpc32/64/80
, embora seja ignorado no Cygwin. Mas lembre-se de que ele modificará o tamanho da mantissa, mas não do expoente, permitindo que a porta se abra para outros tipos de comportamento.Na arquitetura x86_64, SSE é usado como dito por tmandry , então o problema não ocorrerá a menos que você force o antigo x87 FPU para computação FP com
-mfpmath=387
, ou a menos que você compile no modo de 32 bits com-m32
(você precisará do pacote multilib). Eu poderia reproduzir o problema no Linux com diferentes combinações de sinalizadores e versões do gcc:Tentei algumas combinações no Windows ou Cygwin com VC ++ / gcc / tcc, mas o bug nunca apareceu. Suponho que a sequência de instruções geradas não seja a mesma.
Por fim, note que uma forma exótica de evitar esse problema com 4.45 ou 4.55 seria usar
_Decimal32/64/128
, mas o suporte é muito escasso ... Passei muito tempo só para poder fazer um printf comlibdfp
!fonte
Pessoalmente, tive o mesmo problema indo para o outro lado - do gcc para o VS. Na maioria dos casos, acho melhor evitar a otimização. A única vez que vale a pena é quando você está lidando com métodos numéricos envolvendo grandes matrizes de dados de ponto flutuante. Mesmo depois de desmontar, geralmente fico desapontado com as escolhas dos compiladores. Freqüentemente, é mais fácil usar intrínsecos do compilador ou apenas escrever o assembly você mesmo.
fonte