← SEARCHABLE ARCHIVE
Computer Science · 2026-08-08

¿Cómo almacena IEEE 754 los números reales?

Comprender binary32, binary64, redondeo, NaN y error acumulado

INTERACTIVE READING MODE →

¿Cómo almacena IEEE 754 los números reales?

Este artículo está escrito con fines didácticos. El hardware, las opciones del compilador y la implementación del lenguaje pueden cambiar detalles, así que conviene verificar el comportamiento importante en el entorno real.

A muchos principiantes les sorprende que 0.1 + 0.2 no sea exactamente 0.3. El ordenador no ha olvidado sumar. Debe representar infinitos números reales con una cantidad finita de patrones de bits, por lo que la mayoría de los valores se guardan mediante una aproximación cercana.

IEEE 754 no solo normaliza la distribución de bits. También define operaciones, redondeo, infinito, NaN, subdesbordamiento gradual, conversiones y estados excepcionales. Aquí se descomponen binary32 y binary64 y se extraen reglas prácticas para comparar, acumular, representar dinero y serializar.

Punto fijo y coma flotante

El punto fijo elige de antemano la posición de la coma. Si el entero 12345 siempre significa dos decimales, su valor es 123.45. Funciona bien con una escala fija, pero un mismo formato cubre peor magnitudes extremadamente pequeñas y grandes.

La coma flotante se parece a la notación científica.

6.25 = 1.5625 × 2²
0.15625 = 1.25 × 2⁻³

El significando y el exponente se almacenan por separado. El rango es enorme, pero la distancia entre valores adyacentes no es uniforme.

Qué cubre IEEE 754

IEEE 754-2019 especifica formatos binarios y decimales, operaciones, conversiones, redondeo, condiciones excepcionales y su tratamiento predeterminado. Una implementación puede usar hardware, software o ambos.

NombreTipo habitualTotalSignoExponenteFracción almacenadaPrecisión
binary32normalmente float32182324 bits
binary64normalmente double641115253 bits

“Normalmente” es importante. Los nombres de tipos de C y C++ no obligan a todas las plataformas a usar exactamente esos formatos. C++ permite consultar propiedades como std::numeric_limits<T>::is_iec559.

Los campos de binary32

1 bit de signo | 8 bits de exponente | 23 bits de fracción

Para un valor finito normal:

(-1)^s × (1.fraction)₂ × 2^(E - 127)

El exponente usa un sesgo de 127. Un significando binario normal siempre comienza por 1, así que ese bit se hace implícito. Los 23 bits almacenados aportan por tanto 24 bits de precisión. binary64 usa sesgo 1023 y alcanza 53 bits de precisión.

Codificar 6.25 a mano

6.25₁₀ = 110.01₂ = 1.1001₂ × 2²

El signo es 0. El exponente almacenado vale 2 + 127 = 129, o 10000001₂. Tras omitir el 1 inicial, la fracción es 1001000...0.

0 10000001 10010000000000000000000

En hexadecimal es 0x40C80000.

float value = 6.25f;
std::uint32_t bits = std::bit_cast<std::uint32_t>(value);

Patrones especiales del exponente

ExponenteFracciónSignificado
todo cerotodo cerocero con signo
todo cerodistinta de ceronúmero subnormal
1 a 254cualquieranúmero finito normal
todo unotodo ceroinfinito positivo o negativo
todo unodistinta de ceroNaN

Por ello los exponentes normales de binary32 abarcan de -126 a 127.

Cero positivo y negativo

Cuando exponente y fracción son cero, el signo se conserva. Existen +0 y -0. Se comparan como iguales, aunque algunas operaciones mantienen la dirección.

double p = 0.0;
double n = -0.0;

std::cout << (p == n) << '\n';
std::cout << std::signbit(n) << '\n';
std::cout << 1.0 / p << '\n';
std::cout << 1.0 / n << '\n';

Con el comportamiento IEEE habitual, las últimas divisiones producen infinito positivo y negativo.

Números subnormales

La regla del 1 implícito dejaría un hueco entre cero y el menor valor normal. Cuando el exponente almacenado es cero y la fracción no lo es, se usa:

(-1)^s × (0.fraction)₂ × 2^-126

Los subnormales se acercan gradualmente a cero perdiendo precisión. El menor normal positivo de binary32 es aproximadamente 1.17549435 × 10^-38; el menor subnormal positivo, 1.40129846 × 10^-45.

Algunos entornos SIMD o GPU convierten subnormales a cero por rendimiento. El código numérico debe conocer este modo.

Infinito y NaN

Un exponente de unos y una fracción cero representan infinito. El desbordamiento o la división de un valor finito no nulo por cero pueden producirlo con el tratamiento predeterminado.

Si la fracción no es cero, el valor es NaN, Not a Number. 0.0 / 0.0 o la raíz real de un número negativo son ejemplos.

NaN no está ordenado y ni siquiera es igual a sí mismo.

double nan = std::numeric_limits<double>::quiet_NaN();
std::cout << (nan == nan) << '\n';
std::cout << std::isnan(nan) << '\n';

Debe detectarse con std::isnan. El soporte de quiet NaN, signaling NaN y payloads varía entre lenguajes y hardware.

Por qué 0.1 no es exacto

0.1 es la fracción 1 / 10. Una fracción reducida solo tiene expansión binaria finita si el denominador no contiene factores primos distintos de 2. Diez también contiene 5, por lo que los bits se repiten.

0.1₁₀ = 0.000110011001100110011...₂

Binary64 conserva 53 bits significativos y redondea el resto.

std::cout << std::setprecision(20) << 0.1 << '\n';

Un resultado típico es 0.10000000000000000555. La conocida diferencia de 0.1 + 0.2 procede del redondeo de las entradas y del resultado de la suma.

El espaciado crece con la magnitud

Los valores de coma flotante no están distribuidos uniformemente. Binary32 tiene 24 bits de precisión y representa exactamente todos los enteros hasta 16,777,216. Después, los valores adyacentes comienzan a diferir en 2 o más.

float a = 16'777'216.0f;
float b = a + 1.0f;
std::cout << (a == b) << '\n';

En binary32 normalmente es verdadero porque no existe un valor independiente para a + 1 en esa magnitud.

Epsilon no es una tolerancia universal

std::numeric_limits<double>::epsilon() es la distancia entre 1 y el siguiente double representable. No sirve sin cambios para todas las magnitudes. Una comparación práctica suele combinar tolerancia absoluta cerca de cero y relativa en otros rangos.

bool almostEqual(double a, double b,
                 double absoluteTolerance = 1e-12,
                 double relativeTolerance = 1e-9)
{
    double difference = std::abs(a - b);
    if (difference <= absoluteTolerance)
    {
        return true;
    }
    double scale = std::max(std::abs(a), std::abs(b));
    return difference <= scale * relativeTolerance;
}

Las tolerancias deben proceder de las unidades y requisitos del problema.

Modos de redondeo y cinco estados excepcionales

Cuando el resultado exacto queda entre dos valores, la dirección de redondeo elige el almacenado. Un valor predeterminado común redondea al más próximo y, en un empate exacto, escoge el de último bit par.

C y C++ exponen direcciones y banderas mediante <fenv.h> y <cfenv>.

Los cinco estados IEEE son invalid operation, division by zero, overflow, underflow e inexact. Normalmente no son un throw de C++; se produce un resultado predeterminado y se registra una bandera.

std::feclearexcept(FE_ALL_EXCEPT);
volatile double one = 1.0;
volatile double zero = 0.0;
volatile double result = one / zero;

if (std::fetestexcept(FE_DIVBYZERO))
{
    std::cout << "division by zero flag\n";
}

Orden de las operaciones y error acumulado

Cada resultado intermedio se redondea, por lo que la suma finita no siempre es asociativa.

double large = 1e16;
double small = 1.0;

double first = (large + small) - large;
double second = small + (large - large);

La primera expresión puede perder small. En sumas largas pueden ayudar Kahan summation, sumar por parejas valores de magnitud similar o usar un acumulador más ancho.

Dinero y datos exactos

La coma flotante binaria encaja bien con gráficos, coordenadas y muchos cálculos científicos, pero no suele ser apropiada para dinero sujeto a reglas decimales exactas.

  • Guardar la unidad mínima, como céntimos, en un entero.
  • Usar un tipo decimal o de punto fijo.
  • Definir explícitamente cuándo y cómo se redondea.

Los identificadores y contadores exactos también pertenecen a tipos enteros.

Serialización y comprobación

Al cruzar un límite de archivo o red se deben definir binary32 o binary64, el orden de bytes y si se conservan el cero con signo y los payloads de NaN. Para recorridos de texto, C++ ofrece max_digits10 como cantidad suficiente de dígitos.

std::numeric_limits permite consultar is_iec559, radix, digits, min(), denorm_min() y max(). Para tipos flotantes, min() es el menor normal positivo; el finito más negativo se obtiene con lowest().

Referencias

Resumen

IEEE 754 usa signo, significando y exponente para abarcar un rango enorme con pocos bits. El precio son aproximaciones, separaciones dependientes de la magnitud, valores especiales y redondeo intermedio. Al entender un único modelo, 0.1 + 0.2, las comparaciones con NaN, el temblor de coordenadas grandes y el error acumulado dejan de parecer problemas aislados.