Float Exposed
(float.exposed)- 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
floatespecí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
- (−12)02×102(100010012 − 011111112)×1.011111110010100000000002
- 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
- 1×210×1.4967041015625
- 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
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
floatcomo la cadena decimal más corta posible y a la vez inequívoca” Por ejemplo, si usasfloatde precisión simple, necesitas hasta 9 dígitos de precisión decimal para identificar de forma única unfloatPor eso tienes que usar un patrónprintfcomo%.9gPero en ese caso0.1termina imprimiéndose como algo feo como0.100000001Entonces 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 valoresfloatcon 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 esteMe pregunto si habrá una forma más eficiente de encontrar la representación más corta sin repetir
printf/scanfEste problema sí importa de verdad Puede verse como el problema de convertir un
floatespecí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 bibliotecadouble-conversionde Google implementa los dos primerosSí hay una mejor forma de obtenerlo sin un loop de
printf/scanfIncluso solo conprintf("%f", ...)se puede El algoritmo real para convertir defloatastringes 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 nombreNo 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_digits10https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10.htmlNo 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ónSi 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
floatcasi pueden usarse como comparaciones de enteros Para decidira > b, basta con reinterpretaraybcomo enteros con signo y compararlos directamente Eso funciona (casi) bien O sea, el siguiente valorfloatmás grande es simplemente sumar 1 al patrón de bits interpretado como entero Por ejemplo, si empiezas con0.0comofloaty le sumas 1 mediante suma entera, eso ya es el siguiente valorfloat(denormal, el hueco más pequeño) Así es como también se implementanextafterCuando entiendes que los valoresfloatsiguen 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 todoDicho 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 unfloatcomo si fueraint, 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 aplicandoComo 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)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
floatal tener que representar valores mayoresEs 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
floatCuando 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 enfloatDe 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", comoa + (b + c) ≠ (a + b) + cTambién hubo problemas en el mundo real: el sistema de misiles Patriot acumulaba el tiempo confloat, 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
floatde 32 bits Hay muchos artículos y videos al respecto, muy recomendablesEsta 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
floatUna ventaja de ese sitio es que también muestra el error de conversión, aunque no soporta double precisionfloatpuede parecer obvio, pero si apenas estás aprendiendo, ahí sí hace falta una explicación extraTodavía no lo han compartido en estos comentarios, pero mi sitio favorito sobre
floates https://0.30000000000000004.com/En
floatde 32 bits, el “entero más interesante” es16777217(en 64 bits,9007199254740992) Es un caso borde divertido para tener presente en pruebasEn
floatde 64 bits,9007199254740991esNumber.MAX_SAFE_INTEGERen JavaScript Ese valor no es par, y el siguiente valor9007199254740992también es seguro por sí mismo, pero9007199254740993, que claramente ya no es seguro, se redondea y deja de poder distinguirseEn
floatde 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 unfloatpuede representar “exactamente” Por ejemplo, enfloatde 32 bits, el siguiente valor representable después de±16,777,216.0es±16,777,218.0±16,777,217.0no 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 altoMe 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
Estaría muy bueno que también soportara los distintos formatos
fp8que se han incorporado recientemente en las GPU