3 puntos por GN⁺ 2025-09-13 | 1 comentarios | Compartir por WhatsApp
  • Este artículo explica cómo se almacenan y representan en memoria los valores de punto flotante (float)
  • Se enfoca en cómo convertir sus formas hexadecimal y decimal al valor numérico real
  • Explica la definición y el papel de las secciones de signo (Sign), exponente (Exponent) y significando (Significand)
  • Incluye ejemplos de cómo interpretar exactamente qué valor binario y decimal representa un valor float específico
  • También menciona cómo calcular la diferencia (Delta) entre valores representables

Análisis de la estructura de almacenamiento de los valores de punto flotante

  • Existen varios formatos de punto flotante como halfb, float, double, etc.
  • Cada valor puede inspeccionarse en memoria como Raw Hexadecimal Integer Value (valor entero hexadecimal sin procesar) y Raw Decimal Integer Value (valor entero decimal sin procesar)
  • Los datos hexadecimales se vinculan con la notación real de punto flotante mediante Hexadecimal Form ("%a")
  • La posición de cada valor se muestra como Significand–Exponent Range (posición dentro del rango significando–exponente)

Cómo interpretar los valores binarios y decimales

  • Un número de punto flotante puede expresarse en Base-2 (expresión evaluada en binario) de la siguiente manera:
    • (−12)02×​102(100010012 − 011111112)​×​1.011111110010100000000002
      → corresponde a la evaluación numérica mediante una expresión binaria
  • En Base-10 (expresión evaluada en decimal) toma esta forma:
    • 1×​210×​1.4967041015625
      → se expresa como el producto de 2 elevado a la décima potencia y la parte fraccionaria
  • También se muestra el valor decimal exacto al hacer la conversión:
    • presentado en una forma como 1.532625×​103

Cálculo de la distancia a los valores adyacentes (Delta)

  • La Delta (separación) entre valores representables tiene un significado importante
  • Se proporciona por separado la distancia al siguiente o al anterior valor representable (Delta to Next/Previous Representable Value)
    • Ejemplo: ±1.220703125×​10-4
  • Esta separación está relacionada con las cifras significativas / precisión del valor de punto flotante

Resumen

  • La representación en memoria de los números de punto flotante y el principio de conversión entre binario y decimal
  • Explicación de la estructura de sign, exponent, significand
  • También se organiza la información sobre el rango representable y la separación entre valores adyacentes

1 comentarios

 
GN⁺ 2025-09-13
Opiniones de Hacker News
  • Sobre este tema, esta explicación es la mejor: https://fabiensanglard.net/floating_point_visually_explained/ Me topé con este artículo cuando empecé a usar Hacker News, y me dio motivación para que este tipo de contenido siguiera existiendo en la plataforma: https://news.ycombinator.com/item?id=29368529

    • Puede parecer que estoy demasiado cargado hacia lo matemático, pero esa explicación tampoco me pareció tan fácil Si quieres una explicación realmente simple sobre punto flotante: ofrece aproximadamente la misma cantidad de precisión en bits sin importar la escala Es decir, ya sea un número mucho menor que 1, cerca de 1 o muy grande, puedes esperar casi el mismo nivel de exactitud en los bits más significativos Esa es la propiedad clave, pero no es tan fácil interiorizarla

    • Esto encaja muy bien con el contexto del blog que publicó recientemente el equipo de TM https://news.ycombinator.com/item?id=45200925

    • Nunca había visto algo tan bien explicado, así que agradezco que lo compartieras

  • Uno de los problemas que me tuvo pensando durante mucho tiempo fue “cómo representar un valor float como la cadena decimal más corta posible y a la vez inequívoca” Por ejemplo, si usas float de precisión simple, necesitas hasta 9 dígitos de precisión decimal para identificar de forma única un float Por eso tienes que usar un patrón printf como %.9g Pero en ese caso 0.1 termina imprimiéndose como algo feo como 0.100000001 Entonces normalmente se redondea y se muestra con 6 dígitos; si usas %.6g, un valor decimal ingresado con hasta 6 dígitos puede imprimirse igual al valor almacenado Pero para valores que salen como resultado de cálculos, eso deja de ser seguro para round-trip Esto importa especialmente cuando necesitas comparar valores float con exactitud (por ejemplo, para detectar si cambiaron datos) La idea que se me ocurrió fue imprimir primero con 6 dígitos; si al parsearlo vuelve a salir el mismo valor binario, usar ese, y si no, repetir con 7, 8 y hasta 9 dígitos hasta encontrar la representación decimal más corta Mi algoritmo era este

    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;
      }
    }
    

    Me pregunto si habrá una forma más eficiente de encontrar la representación más corta sin repetir printf/scanf

    • Este problema sí importa de verdad Puede verse como el problema de convertir un float específico en una cadena “normalizada” (bajo la condición de que sea la representación válida más cercana) Por eso existen varios algoritmos eficientes como Dragon4, Grisu3, Ryu y Dragonbox La biblioteca double-conversion de Google implementa los dos primeros

    • Sí hay una mejor forma de obtenerlo sin un loop de printf/scanf Incluso solo con printf("%f", ...) se puede El algoritmo real para convertir de float a string es bastante complejo Un buen algoritmo reciente es https://github.com/ulfjack/ryu Según recuerdo, más recientemente salió uno todavía más eficiente, aunque no recuerdo el nombre

    • No hace falta prestarle demasiada atención a las opiniones negativas; aunque quizá no sea la mejor forma, normalmente funciona lo suficientemente bien (si no tiene errores) De hecho, a mí me pasó algo parecido: una vez quise encontrar un vector que se convirtiera en el mismo vector tras una rotación Euler (5°, 5°, 0), y fui moviendo aleatoriamente un vector muy poco para ver si se acercaba más al vector objetivo Hice millones de iteraciones y obtuve el resultado en unos segundos en Python A nivel de biblioteca sería ineficiente, pero para mi caso de uso me dejó muy satisfecho

    • Podría servirte std::numeric_limits<float>::max_digits10 https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10.html

    • No tiene sentido, y jamás deberías usar sscanf() Si conviertes a entero sin signo para serializar y restaurar, es reversible sin pérdida de información

      double f = 0.0/0.0; // puede requerir flag de soft error en algunos 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));
      

      Si necesitas una representación más corta, usa una heurística que permita reconstrucción exacta, siempre que garantice la precisión original (por ejemplo, idempotencia)

  • Mi tip favorito sobre FP es que las comparaciones de float casi pueden usarse como comparaciones de enteros Para decidir a > b, basta con reinterpretar a y b como enteros con signo y compararlos directamente Eso funciona (casi) bien O sea, el siguiente valor float más grande es simplemente sumar 1 al patrón de bits interpretado como entero Por ejemplo, si empiezas con 0.0 como float y le sumas 1 mediante suma entera, eso ya es el siguiente valor float (denormal, el hueco más pequeño) Así es como también se implementa nextafter Cuando entiendes que los valores float siguen el mismo orden que las comparaciones de enteros, se vuelve mucho más natural Claro, hay excepciones: NaN, infinito, -0, etc. Tiene varias aplicaciones útiles, aunque no sirve para todo

    • Dicho así no es exactamente cierto Sí aplica para positivos o comparaciones entre positivo y negativo, pero entre negativos no El punto flotante estándar (float) usa sign-magnitude, mientras que los enteros con signo modernos usan complemento a dos En negativos, la dirección de la comparación de magnitud entre ambos se invierte Si incrementas un float como si fuera int, normalmente avanzas hacia un valor de mayor “magnitud” dentro del mismo signo Es decir, con positivos subes, y con negativos bajas hacia números más negativos En enteros siempre subes, o caes en overflow Más precisamente, sería como decir que coincide con una comparación de enteros sign-magnitude Claro, las salvedades que mencionaste siguen aplicando

    • Como referencia, el algoritmo de comparación total de punto flotante del estándar de Rust, que también ordena NaN, es este (recomendado por IEEE 751)

      let mut left = self.to_bits() as i32;
      let mut right = other.to_bits() as i32;
      
      // Para números negativos, invertir todos los bits excepto el signo
      // hace que el orden se alinee de forma similar a una comparación
      // de enteros en complemento a dos
      
      left ^= (((left >> 31) as u32) >> 1) as i32;
      right ^= (((right >> 31) as u32) >> 1) as i32;
      
      left.cmp(&right)
      

      Ver algoritmo completo

  • Vi este tema en mi curso de Game AI de OMSCS, en un caso sobre precauciones al representar posiciones de objetos de juego con punto flotante Es peligroso porque, cuanto más lejos estás del origen o punto de referencia, más precisión pierde el float al tener que representar valores mayores

    • Es interesante que este fenómeno haya quedado plasmado como el mito de las Far Lands en Minecraft O sea, cuanto más te alejas del origen del mundo, más empiezan a comportarse raro la generación del terreno y la física, y muchísimo más lejos todo ya se rompe por completo Tiene un aire medio ocultista, como si las leyes de la realidad se fueran desmoronando poco a poco Y todo eso por los límites de precisión del float

    • Cuando sumas muchos números entre 0 y 1 usando float, comparar la suma secuencial simple contra sumarlos por pares y luego volver a sumar esos resultados muestra que el método por pares es mucho más preciso Es un ejemplo de lo grave que puede ser el error acumulado en float De hecho, hubo casos reales problemáticos donde este tipo de error se ignoró Donald Knuth explica estas verdades básicas del punto flotante en "The Art of Computer Programming", como a + (b + c) ≠ (a + b) + c También hubo problemas en el mundo real: el sistema de misiles Patriot acumulaba el tiempo con float, y el error se iba acumulando hasta desviarse totalmente del objetivo, por lo que había que reiniciarlo Había que reiniciarlo cada 24 horas, y al final el software del sistema fue corregido También ha habido casos de grandes estructuras que colapsaron por errores de punto flotante (porque un valor de grosor se calculó demasiado delgado)

    • Primero hay que definir las condiciones de borde para establecer cuánta precisión se necesita Entonces también puedes calcular de antemano la distancia mínima y máxima Si el mundo es demasiado grande, hay que dividirlo en sectores o manejar por separado coordenadas globales y locales (por ejemplo, No Man's Sky) Un juego no deja de ser tramoya Con Double-Precision suele alcanzar para la mayoría de los casos Lo importante es recordar no sumar valores pequeños y grandes juntos

    • Kerbal Space Program recurrió a ingeniería bastante inteligente para intentar representar un sistema solar entero usando solo float de 32 bits Hay muchos artículos y videos al respecto, muy recomendables

  • Esta visualización está divertida, y me parece interesante porque se ve parecida al CIDR range calculator que hice hace tiempo para ayudar a entender rangos de red Este tipo de visualizaciones son muy útiles

  • Antes usaba https://www.h-schmidt.net/FloatConverter/IEEE754.html para explorar representaciones de float Una ventaja de ese sitio es que también muestra el error de conversión, aunque no soporta double precision

    • Yo también revisé rápido los comentarios para ver si alguien ya lo había mencionado; de verdad es una muy buena página web Pero el sitio presentado por el OP explica de forma muy intuitiva, mediante gráficos, la estructura de partición del espacio numérico El eje vertical está en escala logarítmica, y el horizontal es lineal en cada fila, pero normalizado de acuerdo con cada intervalo logarítmico Para quien ya entiende bien los float puede parecer obvio, pero si apenas estás aprendiendo, ahí sí hace falta una explicación extra
  • Todavía no lo han compartido en estos comentarios, pero mi sitio favorito sobre float es https://0.30000000000000004.com/

  • En float de 32 bits, el “entero más interesante” es 16777217 (en 64 bits, 9007199254740992) Es un caso borde divertido para tener presente en pruebas

    • En float de 64 bits, 9007199254740991 es Number.MAX_SAFE_INTEGER en JavaScript Ese valor no es par, y el siguiente valor 9007199254740992 también es seguro por sí mismo, pero 9007199254740993, que claramente ya no es seguro, se redondea y deja de poder distinguirse

    • En float de 64 bits en realidad es exactamente ±9,007,199,254,740,993.0 :-) Como referencia, esos valores significan el primer valor después del mayor entero que un float puede representar “exactamente” Por ejemplo, en float de 32 bits, el siguiente valor representable después de ±16,777,216.0 es ±16,777,218.0 ±16,777,217.0 no puede representarse, así que normalmente se redondea hacia cero o algo similar Estos límites de precisión y problemas de redondeo suelen pasarse por alto

  • Me alegra que exista IEEE754, pero IEEE754 no es perfecto y creo que formatos como posit son mejores (suponiendo que no haya soporte por hardware) Los rational de bignum son todavía superiores a ambos, pero también son los más lentos

    • IEEE754 es una solución de compromiso que intenta cubrir muchos requisitos Algunos formatos alternativos son mejores en ciertas áreas, pero peores en otras
  • Estaría muy bueno que también soportara los distintos formatos fp8 que se han incorporado recientemente en las GPU