Tentei verificar onde float
perde a capacidade de representar exatamente grandes números inteiros. Então, escrevi este pequeno trecho:
int main() {
for (int i=0; ; i++) {
if ((float)i!=i) {
return i;
}
}
}
Este código parece funcionar com todos os compiladores, exceto o clang. O Clang gera um loop infinito simples. Godbolt .
Isso é permitido? Se sim, é um problema de QoI?
c++
floating-point
clang
Geza
fonte
fonte
gcc
faz a mesma otimização de loops infinitos se você compilar com-Ofast
, então é uma otimizaçãogcc
considerada insegura, mas pode fazer isso.ucomiss xmm0,xmm0
para se comparar(float)i
a si mesmo. Essa foi sua primeira pista de que seu código-fonte C ++ não significa o que você pensava. Você está afirmando que tem este loop para imprimir / devolver16777216
? Com que compilador / versão / opções isso? Porque isso seria um bug do compilador. O gcc otimiza corretamente o seu códigojnp
como o ramo do loop ( godbolt.org/z/XJYWeu ): continue o loop enquanto os operandos!=
não forem NaN.-ffast-math
opção que está implicitamente ativada por-Ofast
que permite ao GCC aplicar otimizações de ponto flutuante inseguras e, assim, gerar o mesmo código que o Clang. O MSVC se comporta exatamente da mesma maneira: sem/fp:fast
, ele gera um monte de código que resulta em um loop infinito; com/fp:fast
, ele emite uma únicajmp
instrução. Estou supondo que, sem ativar explicitamente as otimizações de FP inseguras, esses compiladores ficam presos aos requisitos IEEE 754 relativos aos valores NaN. É bastante interessante que o Clang não o faça, na verdade. Seu analisador estático é melhor. @ 12345ieee(float) i
difere do valor matemático dei
, o resultado (o valor retornado nareturn
instrução) seria 16.777.217, não 16.777.216.Respostas:
Como @Angew apontou , o
!=
operador precisa do mesmo tipo em ambos os lados.(float)i != i
resulta na promoção do RHS para flutuar também, então fizemos(float)i != (float)i
.g ++ também gera um loop infinito, mas não otimiza o trabalho de dentro dele. Você pode ver que ele converte int-> float com
cvtsi2ss
e fazucomiss xmm0,xmm0
para comparar(float)i
consigo mesmo. (Essa foi sua primeira pista de que seu código-fonte C ++ não significa o que você achou que ele gostava de @resposta de Angew explica.)x != x
só é verdadeiro quando está "desordenado" porquex
era NaN. (INFINITY
compara igual a si mesmo na matemática IEEE, mas NaN não.NAN == NAN
é falso,NAN != NAN
é verdadeiro).gcc7.4 e mais antigos otimizam corretamente seu código
jnp
como o ramo do loop ( https://godbolt.org/z/fyOhW1 ): mantenha o loop enquanto os operandosx != x
não forem NaN. (gcc8 e posterior também verificaje
se há uma quebra do loop, deixando de otimizar com base no fato de que sempre será verdadeiro para qualquer entrada não NaN). x86 FP compara PF definido em não ordenado.E, a propósito, isso significa que a otimização do clang também é segura : ele apenas precisa CSE
(float)i != (implicit conversion to float)i
como sendo o mesmo, e provar quei -> float
nunca é NaN para o intervalo possível deint
.(Embora dado que este loop atingirá UB de estouro assinado, é permitido emitir literalmente qualquer asm que desejar, incluindo uma
ud2
instrução ilegal ou um loop infinito vazio, independentemente do que o corpo do loop realmente era.) Mas ignorando o UB de estouro assinado , essa otimização ainda é 100% legal.O GCC falha em otimizar o corpo do loop, mesmo
-fwrapv
para tornar o estouro de inteiro com sinal bem definido (como complemento de 2). https://godbolt.org/z/t9A8t_Mesmo habilitando
-fno-trapping-math
não ajuda. (O padrão do GCC infelizmente é habilitar-ftrapping-math
mesmo que a implementação do GCC esteja quebrada / com erros .) Int-> conversão float pode causar uma exceção inexata de FP (para números muito grandes para serem representados exatamente), então, com exceções possivelmente desmascaradas, é razoável não otimize o corpo do loop. (Porque a conversão16777217
para float pode ter um efeito colateral observável se a exceção inexata for desmascarada.)Mas com
-O3 -fwrapv -fno-trapping-math
, é 100% de otimização perdida não compilar isso em um loop infinito vazio. Sem#pragma STDC FENV_ACCESS ON
, o estado dos sinalizadores fixos que registram exceções de FP mascaradas não é um efeito colateral observável do código. Nãoint
->float
conversão pode resultar em NaN, entãox != x
não pode ser verdade.Todos esses compiladores são otimizados para implementações C ++ que usam IEEE 754 de precisão única (binary32)
float
e 32 bitsint
.O loop corrigido de bug
(int)(float)i != i
teria UB em implementações C ++ com estreito de 16 bitsint
e / ou mais amplofloat
, porque você atingiu o UB de estouro de inteiro com sinal antes de atingir o primeiro inteiro que não era exatamente representável como afloat
.Mas o UB sob um conjunto diferente de opções definidas pela implementação não tem consequências negativas ao compilar para uma implementação como gcc ou clang com o x86-64 System V ABI.
BTW, você poderia calcular estaticamente o resultado deste loop de
FLT_RADIX
eFLT_MANT_DIG
, definido em<climits>
. Ou pelo menos você pode em teoria, sefloat
realmente se encaixa no modelo de um float IEEE em vez de algum outro tipo de representação de número real como um Posit / unum.Não tenho certeza de quanto o padrão ISO C ++ esclarece sobre
float
comportamento e se um formato que não fosse baseado em expoentes de largura fixa e campos de significand seria compatível com os padrões.Nos comentários:
Você está afirmando que tem este loop para imprimir / devolver
16777216
?Atualização: como esse comentário foi excluído, acho que não. Provavelmente, o OP está apenas citando o
float
antes do primeiro inteiro que não pode ser representado exatamente como 32 bitsfloat
. https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values ou seja, o que eles esperavam verificar com este código bugado.A versão corrigida do bug, é claro
16777217
, imprimir , o primeiro inteiro que não exatamente representável, em vez do valor anterior.(Todos os valores flutuantes mais altos são inteiros exatos, mas são múltiplos de 2, depois 4, depois 8, etc. para valores de expoentes maiores que a largura do significante. Muitos valores inteiros maiores podem ser representados, mas 1 unidade no último lugar (do significando) é maior que 1, portanto não são inteiros contíguos. O maior finito
float
está logo abaixo de 2 ^ 128, o que é muito grande para mesmoint64_t
.)Se algum compilador sair do loop original e imprimi-lo, será um bug do compilador.
fonte
frapw
, mas tenho certeza que o GCC 10-ffinite-loops
foi projetado para situações como essa.Observe que o operador integrado
!=
requer que seus operandos sejam do mesmo tipo e o conseguirá usando promoções e conversões, se necessário. Em outras palavras, sua condição é equivalente a:Isso nunca deve falhar e, portanto, o código irá eventualmente estourar
i
, dando ao seu programa um comportamento indefinido. Qualquer comportamento é, portanto, possível.Para verificar corretamente o que deseja verificar, você deve lançar o resultado de volta para
int
:fonte
static_cast<int>(static_cast<float>(i))
?reinterpret_cast
é UB óbvio lá(int)(float)i != i
é UB? Como você conclui isso? Sim, depende das propriedades definidas pela implementação (porquefloat
não é necessário ser IEEE754 binary32), mas em qualquer implementação dada é bem definido, a menos quefloat
possa representar exatamente todos osint
valores positivos , então obtemos UB de estouro de inteiro com sinal. ( en.cppreference.com/w/cpp/types/climits defineFLT_RADIX
eFLT_MANT_DIG
determina isso). Em geral, imprimindo coisas definidas pela implementação, comostd::cout << sizeof(int)
não é UB ...reinterpret_cast<int>(float)
não é exatamente UB, é apenas um erro de sintaxe / malformado. Seria bom se essa sintaxe permitisse trocadilhos de floatint
como uma alternativa paramemcpy
(que é bem definido), masreinterpret_cast<>
só funciona em tipos de ponteiro, eu acho.x != x
é verdade. Veja ao vivo no coliru . Em C também.