Veja este trecho:
int main() {
double v = 1.1;
return v == 1.1;
}
Em compilações de 32 bits, este programa retorna 0, se -fexcess-precision=standard
for especificado. Sem ele, o programa retorna 1.
Por que há uma diferença? Olhando para o código assembly ( godbolt ), parece que com -fexcess-precision=standard
, o gcc usa 1.1
como uma long double
constante (ele carrega a constante como TBYTE
). Por que ele faz isso?
Primeiro pensei que fosse um bug, mas encontrei este comentário de bug do gcc , parece que esse comportamento é intencional, ou pelo menos não é inesperado.
Isso é um problema de QoI? Eu entendo que a comparação é executada usando long double
precisão, mas ainda assim, my 1.1
não é um long double
literal. O estranho é que se eu lançar o 1.1
at na comparação to double
(que já é um double
), o problema desaparece.
(Outra coisa estranha é que o GCC faz o carregamento e a comparação duas vezes, veja as fucomip
instruções duplas. Mas ele faz isso mesmo no modo de 64 bits. Entendo que no meu link godbolt, a otimização está desativada, mas ainda assim, há apenas uma comparação no meu código, por que o GCC compara duas vezes?)
Aqui está o código asm, sem -fexcess-precision=standard
:
main:
push ebp
mov ebp, esp
and esp, -8
sub esp, 16
fld QWORD PTR .LC0
fstp QWORD PTR [esp+8]
fld QWORD PTR [esp+8]
fld QWORD PTR .LC0
fucomip st, st(1)
fstp st(0)
setnp al
mov edx, 0
fld QWORD PTR [esp+8]
fld QWORD PTR .LC0
fucomip st, st(1)
fstp st(0)
cmovne eax, edx
movzx eax, al
leave
ret
.LC0:
.long -1717986918
.long 1072798105
E aqui está:
main:
push ebp
mov ebp, esp
and esp, -8
sub esp, 16
fld QWORD PTR .LC0
fstp QWORD PTR [esp+8]
fld QWORD PTR [esp+8]
fld TBYTE PTR .LC1
fucomip st, st(1)
setnp al
mov edx, 0
fld TBYTE PTR .LC1
fucomip st, st(1)
fstp st(0)
cmovne eax, edx
movzx eax, al
leave
ret
.LC0:
.long -1717986918
.long 1072798105
.LC1:
.long -858993459
.long -1932735284
.long 16383
Em C, é permitido (conforme indicado via
FLT_EVAL_METHOD
) que um literal de ponto flutuante possa conter um valor com mais previsão, conforme permitido por seu tipo e que, ao mesmo tempo, operadores de ponto flutuante sejam avaliados com uma precisão maior do que os tipos de operando permitem.Nesse caso,
v == 1.1
pode ser falso porque o literal1.1
, embora do tipodouble
, não será arredondado paradouble
precisão, mas==
ainda o compara com maior precisão com o valor armazenado,v
que ainda deve ser arredondado para um valor representável pordouble
.Em C++, embora ainda seja permitido que operações de ponto flutuante sejam avaliadas com maior precisão, o valor de um literal de ponto flutuante ainda precisa ser arredondado para um valor representável em seu tipo.
No entanto, isso interage incorretamente com a especificação incorporada de C, como
FLT_EVAL_METHOD
, e desvia de C aparentemente sem motivo, então a questão da precisão de valores literais de ponto flutuante ainda é uma questão em aberto, veja https://cplusplus.github.io/CWG/issues/2752.html e https://github.com/cplusplus/papers/issues/1584 .Sem o
-fexcess-precision=standard
sinalizador, o GCC não se comporta de forma alguma em conformidade com o padrão e pode até interpretar o valor dev
como uma precisão maior do que seu tipo permite, o que não é permitido nem pelo padrão C nem pelo C++. (Atribuição, conversão e inicialização devem sempre forçar um arredondamento para um valor representável no tipo real.) Com isso, pode acontecer quev == 1.1
seja verdadeiro novamente em virtude de tanto o literal quanto o valor dev
conforme recuperado do literal nunca serem arredondados para umdouble
valor representável.Tudo isso é tipicamente relevante em, por exemplo, uma compilação x86 de 32 bits, onde
FLT_EVAL_METHOD
frequentemente será definido como2
, significando que a precisão mais alta mencionada acima deve sempre ser escolhida como se o tipo fosselong double
. Isso é para dar suporte à manutençãodouble
como tipo de 64 bits ao executar operações de ponto flutuante com precisão de 80 bits na FPU x87. Normalmente, essa escolha paraFLT_EVAL_METHOD
torna o comportamento determinístico no sentido de que é possível dizer exatamente onde um arredondamento é aplicado, mas observe que o padrão (-fexcess-precision=fast
) do GCC não será consistente em se e onde o arredondamento será aplicado.Dado que
FLT_EVAL_METHOD
é2
e dadas as escolhas para tipos de ponto flutuante, seguindo as regras C,v == 1.1
avaliar como falso é o único comportamento correto em conformidade com o padrão. Para C++ isso é diferente, mas não está claro se isso é um defeito no padrão C++. Portanto, é um tanto compreensível por que o GCC seguiria o comportamento necessário em C.O fato de que
v == 1.1
pode ser avaliado como falso é muito intencional e os programadores precisam estar cientes do comportamento de excesso de precisão, a menos que tenham certeza de que seu código só precisa suportar implementações nasFLT_EVAL_METHOD == 0
quais nenhum excesso de precisão será aplicado.Para processadores Intel, long double geralmente é um tipo de ponto flutuante de 80 bits, maior que o double simples de 64 bits.
E então faz sentido: você pega 1,1 com precisão de 80 bits, arredonda para baixo para precisão de 64 bits e armazena em um double.
Então você pega 1.1 com precisão de 80 bits e compara com 1.1 com precisão de 64 bits. Os valores são diferentes porque um tem 16 bits a mais.