3 pontos por GN⁺ 2025-09-13 | 1 comentários | Compartilhar no WhatsApp
  • Este texto explica como valores de ponto flutuante (float) são armazenados na memória e representados
  • O foco está em como converter entre a forma hexadecimal e decimal do valor e o valor numérico real
  • Explica a definição das áreas de sinal (Sign), expoente (Exponent) e significando (Significand) e o papel de cada uma
  • Inclui exemplos de como interpretar exatamente quais valores binários e decimais um determinado valor float representa
  • Também menciona o cálculo da diferença (Delta) entre valores representáveis

Análise da estrutura de armazenamento de valores de ponto flutuante

  • Existem vários formatos de ponto flutuante, como halfb float float double
  • Cada valor pode ser verificado na memória como Raw Hexadecimal Integer Value (valor inteiro hexadecimal bruto) e Raw Decimal Integer Value (valor inteiro decimal bruto)
  • Os dados hexadecimais são conectados à notação real de ponto flutuante por meio da Hexadecimal Form ("%a")
  • A posição de cada valor é apresentada como Significand–Exponent Range (posição no intervalo significando–expoente)

Como interpretar valores binários e decimais

  • Um número de ponto flutuante pode ser expresso em Base-2 (expressão avaliada em binário) da seguinte forma:
    • (−12)02×​102(100010012 − 011111112)​×​1.011111110010100000000002
      → trata-se da avaliação numérica por meio de uma expressão binária
  • Em Base-10 (expressão avaliada em decimal), a forma é esta:
    • 1×​210×​1.4967041015625
      → expresso como o produto de 2 elevado à 10ª potência e a parte fracionária
  • O valor decimal exato obtido na conversão também é mostrado:
    • apresentado em uma forma como 1.532625×​103

Cálculo da distância (Delta) até valores vizinhos

  • O Delta (intervalo) entre valores representáveis tem um significado importante
  • São fornecidas separadamente as distâncias até o próximo e o valor representável anterior (Delta to Next/Previous Representable Value)
    • Exemplo: ±1.220703125×​10-4
  • Esse intervalo está relacionado aos algarismos significativos / à precisão do valor de ponto flutuante

Resumo

  • A representação na memória de números de ponto flutuante e o princípio de conversão para binário e decimal
  • Explicação da estrutura sign, exponent, significand
  • Também organiza informações sobre o intervalo de representação e a distância até valores adjacentes

1 comentários

 
GN⁺ 2025-09-13
Opiniões do Hacker News
  • Sobre este tema, esta é a melhor explicação: https://fabiensanglard.net/floating_point_visually_explained/ Encontrei este texto quando comecei a acompanhar o Hacker News, e ele me deu motivação para continuar na plataforma por causa de conteúdos assim: https://news.ycombinator.com/item?id=29368529

    • Talvez eu seja enviesado demais para o lado matemático, mas essa explicação não me pareceu tão fácil assim Se você quer uma explicação realmente simples sobre ponto flutuante: ele fornece aproximadamente a mesma quantidade de precisão em bits, independentemente da escala Ou seja, seja para números muito menores que 1, próximos de 1 ou muito grandes, você pode esperar quase o mesmo nível de precisão nos bits mais significativos Essa é a propriedade central, mas não é fácil internalizá-la

    • Combina muito bem com o contexto do blog escrito recentemente pela equipe da TM https://news.ycombinator.com/item?id=45200925

    • Nunca tinha visto isso explicado tão bem, então agradeço por compartilhar

  • Um dos problemas em que pensei bastante foi “como representar um valor float como a string decimal mais curta e, ainda assim, sem ambiguidades” Por exemplo, ao usar float de precisão simples, são necessários até 9 dígitos de precisão decimal para identificar o float de forma única Por isso é preciso usar um padrão de printf como %.9g Mas, nesse caso, 0.1 acaba sendo impresso como algo feio tipo 0.100000001 Então normalmente se arredonda para 6 dígitos, e com %.6g um valor decimal digitado com até 6 dígitos pode ser exibido igual ao valor armazenado Mas, para valores resultantes de cálculos, isso deixa de ser seguro para round-trip Isso importa especialmente quando é preciso comparar valores float com exatidão (por exemplo, para verificar se dados mudaram) A ideia que tive foi: primeiro imprimir com 6 dígitos e, se ao fazer o parse o mesmo valor binário sair de volta, usar isso; se não, repetir com 7, 8 e 9 dígitos até encontrar a representação decimal mais curta Meu algoritmo é o seguinte

    int out_length;
    char buffer[32];
    for (int prec = 6; prec<=9; prec++) {
      out_length = sprintf(buffer, "%.*g", prec, floatValue);
      if (prec == 9) {
        break;
      }
      float checked_number;
      sscanf(buffer, "%g", &checked_number);
      if (checked_number == floatValue) {
        break;
      }
    }
    

    Fico pensando se existe uma forma mais eficiente de encontrar a representação mais curta sem repetir printf/scanf

    • Esse problema é realmente importante Dá para vê-lo como o problema de produzir uma string “normalizada” para um float específico Por isso existem vários algoritmos eficientes, como Dragon4, Grisu3, Ryu e Dragonbox A biblioteca double-conversion do Google também implementa os dois primeiros

    • Há uma forma melhor de obter isso sem um loop de printf/scanf Dá até para fazer só com printf("%f", ...) O algoritmo real de conversão de float para string é bem complexo Um algoritmo bom e recente é https://github.com/ulfjack/ryu Acho que surgiu recentemente um método ainda mais eficiente, mas não lembro o nome

    • Não precisa se preocupar tanto com as opiniões negativas; embora talvez não seja o melhor método, se não houver erros, geralmente funciona bem o suficiente Já passei por algo parecido: certa vez eu queria encontrar um vetor que, após uma rotação de Euler (5°, 5°, 0), resultasse no mesmo vetor, então fui deslocando vetores aleatoriamente e vendo se se aproximavam mais do vetor de referência Rodei um loop de vários milhões de iterações e consegui o resultado em poucos segundos no Python Em nível de biblioteca seria ineficiente, mas para o meu caso de uso foi mais do que satisfatório

    • Vale a pena ver std::numeric_limits<float>::max_digits10 https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10.html

    • Isso não faz sentido, e sscanf() jamais deveria ser usado Converta para inteiro sem sinal e serialize/restaure assim, que é reversível sem perda de informação

      double f = 0.0/0.0; // pode precisar de flag de soft error em alguns compiladores
      double g;
      char s[9];
      
      assert(sizeof double == sizeof uint64_t);
      
      snprintf(s, 9, "%0" PRIu64, *(uint64_t *)(&f));
      
      snscanf(s, 9, "%0" SCNu64, (uint64_t *)(&g));
      

      Se precisar de uma representação mais curta, use uma heurística com restauração exata circular, desde que o método garanta a precisão original (por exemplo, idempotência)

  • Minha dica favorita sobre FP é que comparação de float quase pode ser usada como comparação de inteiros Para decidir a > b, basta interpretar a e b como inteiros com sinal e comparar normalmente Isso funciona (quase sempre) Ou seja, o próximo valor float maior é simplesmente o padrão de bits reinterpretado como inteiro e incrementado em 1 Por exemplo, se você começar de 0.0 em float e somar 1 como inteiro, esse já é o próximo valor float (denormal, o menor espacinho possível) É assim que nextafter também é implementado Saber que os valores de float seguem a mesma ordem de comparação dos inteiros deixa tudo muito mais intuitivo Claro, há exceções: NaN, infinito, -0 etc. são diferentes Isso é útil em alguns casos, mas não em todos

    • Isso, estritamente falando, não é verdade Vale para positivos ou para comparações entre positivo e negativo, mas entre negativos é diferente Ponto flutuante padrão (float) usa sign-magnitude, enquanto inteiros com sinal modernos usam complemento de dois Entre negativos, a direção da comparação de magnitude se inverte Se você incrementar um float como se fosse um int, normalmente vai para um número de “magnitude” maior dentro do mesmo sinal Ou seja, positivos sobem, e negativos descem para números negativos ainda menores Já inteiros sempre sobem ou estouram em overflow Uma forma mais precisa de dizer isso é que a comparação equivale à de inteiros sign-magnitude Claro, os caveats mencionados continuam valendo

    • A propósito, o algoritmo de ordenação total para ponto flutuante do Rust, em que até NaN pode ser comparado, é o seguinte (recomendado pela IEEE 751)

      let mut left = self.to_bits() as i32;
      let mut right = other.to_bits() as i32;
      
      // Para números negativos, inverter todos os bits exceto o de sinal
      // produz uma ordenação análoga à comparação de inteiros em complemento de dois
      
      left ^= (((left >> 31) as u32) >> 1) as i32;
      right ^= (((right >> 31) as u32) >> 1) as i32;
      
      left.cmp(&right)
      

      Ver o algoritmo completo

  • Vi esse ponto num caso do meu curso de Game AI no OMSCS, sobre cuidados ao representar a posição de objetos do jogo com ponto flutuante Quanto mais longe da origem ou do ponto de referência, mais perigoso fica, porque o float vai gastando precisão para armazenar valores maiores

    • É interessante como esse fenômeno virou algo mítico no Minecraft com as Far Lands Ou seja, quanto mais longe você vai da origem do mundo, mais a geração de terreno e a física começam a ficar estranhas, até que muito mais longe tudo quebra de vez Tem até um certo ar ocultista, como se as leis da realidade fossem se desfazendo aos poucos E tudo isso por causa dos limites de precisão de float

    • Ao somar muitos números entre 0 e 1 em float, comparar uma soma ingênua em sequência com uma soma em pares, somando de dois em dois e depois reunindo os resultados, mostra que o método em pares é muito mais preciso É um exemplo de como o erro acumulado em float pode ser sério Houve casos reais em que esse tipo de erro foi ignorado e causou problemas Donald Knuth explica esses pontos e verdades básicas sobre float, como a + (b + c) ≠ (a + b) + c, em "The Art of Computer Programming" Também houve situações reais em que isso causou problemas: o sistema de mísseis Patriot acumulava tempo em float, o erro ia crescendo e acabava desviando completamente do alvo, exigindo reinicialização Era preciso reiniciar a cada 24 horas, e o software do sistema acabou sendo corrigido Também já houve casos de grandes estruturas desabarem por causa de erro de float (quando uma medida de espessura foi calculada fina demais)

    • Você precisa definir primeiro as condições de contorno para estabelecer quanta precisão é necessária A partir disso, dá para calcular antes as distâncias mínima e máxima Se o mundo ficar grande demais, é preciso dividi-lo em setores ou gerenciar separadamente coordenadas globais/locais (por exemplo, em No Man's Sky) Jogos são, no fim das contas, um tipo de truque de palco Double-Precision já é suficiente para a maioria dos casos O importante é lembrar de não somar valores pequenos com valores grandes ao mesmo tempo

    • Kerbal Space Program usa uma engenharia bem inteligente para tentar representar um sistema solar inteiro só com float de 32 bits Há muitos artigos e vídeos sobre isso, e recomendo bastante

  • Essa visualização é divertida e me chamou atenção por ser parecida, visualmente, com a calculadora de intervalos CIDR que fiz há algum tempo para ajudar a entender faixas de rede Visualizações assim são muito úteis

  • Antigamente, quando eu explorava representações de float, usava https://www.h-schmidt.net/FloatConverter/IEEE754.html O bom desse site é que ele também mostra o erro de conversão, mas não oferece suporte a double precision

    • Eu também dei uma passada pelos comentários para ver se alguém já tinha mencionado isso, e realmente é uma página excelente Mas o site apresentado pelo OP explica de forma muito intuitiva, com gráficos, a estrutura de particionamento do espaço numérico O eixo vertical está em escala logarítmica, e o horizontal é linear em cada linha, mas normalizado para corresponder à faixa logarítmica Para quem já se sente à vontade com float, isso pode parecer óbvio, mas para quem está aprendendo pela primeira vez, é um ponto que pede explicação adicional
  • Ainda não vi este comentário compartilhar isso, mas meu site favorito sobre float é https://0.30000000000000004.com/

  • No float de 32 bits, o “inteiro mais interessante” é 16777217 (e no de 64 bits, 9007199254740992) É divertido conhecer esses edge cases para testes

    • Em float de 64 bits, 9007199254740991 é Number.MAX_SAFE_INTEGER no JavaScript Esse valor não é par, e o próximo valor, 9007199254740992, também é seguro por si só, mas valores claramente inseguros como 9007199254740993 acabam arredondados e deixam de poder ser distinguidos

    • Em float de 64 bits, é exatamente ±9,007,199,254,740,993.0 :-) A propósito, esses valores representam o primeiro número logo após o limite do maior inteiro que float consegue representar “exatamente” Por exemplo, no float de 32 bits, o próximo valor representável depois de ±16,777,216.0 é ±16,777,218.0 ±16,777,217.0 não pode ser representado e normalmente é arredondado em direção ao zero ou algo do tipo Esse tipo de limite de precisão e problema de arredondamento muitas vezes passa despercebido

  • Fico feliz que a IEEE754 exista, mas não acho que ela seja perfeita, e acho que valores como posit seriam melhores (assumindo ausência de suporte em hardware) BigNum rational (racionais) é ainda superior aos dois, mas também é o mais lento

    • IEEE754 é um compromisso que tenta atender a várias exigências Alguns formatos alternativos são melhores em domínios específicos, mas piores em outros
  • Seria muito legal se isso passasse a oferecer suporte aos vários formatos fp8 introduzidos recentemente nas GPUs