Essa otimização de ponto flutuante é permitida?

90

Tentei verificar onde floatperde 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?

Geza
fonte
@geza, gostaria de saber o número resultante!
nada de
5
gccfaz a mesma otimização de loops infinitos se você compilar com -Ofast, então é uma otimização gccconsiderada insegura, mas pode fazer isso.
12345ieee
3
g ++ também gera um loop infinito, mas não otimiza o trabalho de dentro dele. Você pode ver que sim ucomiss xmm0,xmm0para se comparar (float)ia 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 / devolver 16777216? Com que compilador / versão / opções isso? Porque isso seria um bug do compilador. O gcc otimiza corretamente o seu código jnpcomo o ramo do loop ( godbolt.org/z/XJYWeu ): continue o loop enquanto os operandos != não forem NaN.
Peter Cordes
4
Especificamente, é a -ffast-mathopção que está implicitamente ativada por -Ofastque 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 única jmpinstruçã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
Cody Gray
1
@geza: Se o código fizesse o que você pretendia, verificando quando o valor matemático de (float) idifere do valor matemático de i, o resultado (o valor retornado na returninstrução) seria 16.777.217, não 16.777.216.
Eric Postpischil

Respostas:

49

Como @Angew apontou , o !=operador precisa do mesmo tipo em ambos os lados. (float)i != iresulta 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 cvtsi2sse faz ucomiss xmm0,xmm0para comparar (float)iconsigo 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 != xsó é verdadeiro quando está "desordenado" porque xera NaN. ( INFINITYcompara 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 jnpcomo o ramo do loop ( https://godbolt.org/z/fyOhW1 ): mantenha o loop enquanto os operandos x != x não forem NaN. (gcc8 e posterior também verifica jese 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)icomo sendo o mesmo, e provar que i -> floatnunca é NaN para o intervalo possível de int.

(Embora dado que este loop atingirá UB de estouro assinado, é permitido emitir literalmente qualquer asm que desejar, incluindo uma ud2instruçã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 -fwrapvpara tornar o estouro de inteiro com sinal bem definido (como complemento de 2). https://godbolt.org/z/t9A8t_

Mesmo habilitando -fno-trapping-mathnão ajuda. (O padrão do GCC infelizmente é habilitar
-ftrapping-mathmesmo 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ão 16777217para 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ão int-> floatconversão pode resultar em NaN, então x != xnão pode ser verdade.


Todos esses compiladores são otimizados para implementações C ++ que usam IEEE 754 de precisão única (binary32) floate 32 bits int.

O loop corrigido de bug(int)(float)i != i teria UB em implementações C ++ com estreito de 16 bits inte / ou mais amplo float, 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 a float.

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_RADIXe FLT_MANT_DIG, definido em <climits>. Ou pelo menos você pode em teoria, se floatrealmente 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 floatcomportamento 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:

@geza, gostaria de saber o número resultante!

@nada: é 16777216

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 floatantes do primeiro inteiro que não pode ser representado exatamente como 32 bits float. 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 floatestá logo abaixo de 2 ^ 128, o que é muito grande para mesmo int64_t.)

Se algum compilador sair do loop original e imprimi-lo, será um bug do compilador.

Peter Cordes
fonte
3
@SombreroChicken: não, eu aprendi eletrônica primeiro (de alguns livros que meu pai tinha por aí; ele era professor de física), então lógica digital e entrei em CPUs / software depois disso. : P Sempre gostei de entender as coisas do zero, ou se eu começar com um nível superior, gosto de aprender pelo menos algo sobre o nível abaixo que influencia como / por que as coisas funcionam no nível em que estou pensando sobre. (por exemplo, como o ASM funciona e como otimizá-lo é influenciado pelas restrições de design da CPU / coisas da arquitetura da CPU. Que por sua vez vem de física + matemática.)
Peter Cordes
1
O GCC pode não ser capaz de otimizar mesmo com frapw, mas tenho certeza que o GCC 10 -ffinite-loopsfoi projetado para situações como essa.
MCCCS
64

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:

(float)i != (float)i

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:

if ((int)(float)i != i)
Angew não está mais orgulhoso de SO
fonte
8
@ Džuris É UB. Não é ninguém resultado definitivo. O compilador pode perceber que só pode terminar em UB e decidir remover o loop inteiramente.
Ação judicial de Monica de
4
@opa você quer dizer static_cast<int>(static_cast<float>(i))? reinterpret_casté UB óbvio lá
Caleth
6
@NicHartley: Você está dizendo que (int)(float)i != ié UB? Como você conclui isso? Sim, depende das propriedades definidas pela implementação (porque floatnão é necessário ser IEEE754 binary32), mas em qualquer implementação dada é bem definido, a menos que floatpossa representar exatamente todos os intvalores positivos , então obtemos UB de estouro de inteiro com sinal. ( en.cppreference.com/w/cpp/types/climits define FLT_RADIXe FLT_MANT_DIGdetermina isso). Em geral, imprimindo coisas definidas pela implementação, como std::cout << sizeof(int)não é UB ...
Peter Cordes
2
@Caleth: reinterpret_cast<int>(float)não é exatamente UB, é apenas um erro de sintaxe / malformado. Seria bom se essa sintaxe permitisse trocadilhos de float intcomo uma alternativa para memcpy(que é bem definido), mas reinterpret_cast<>só funciona em tipos de ponteiro, eu acho.
Peter Cordes
2
@Peter Just for NaN, x != xé verdade. Veja ao vivo no coliru . Em C também.
Deduplicator