Números Pseudoaleatorios · Tema 40

Error de redondeo

Cada + − × ÷ redondea al binario más cercano: medio ulp que parece nada y decide tu simulación.

01 · Punto de partida

Toda operación miente un poco

En el Tema 39 viste el formato: el double es una grilla finita de 53 bits donde 0,1 ni siquiera existe. Ahora viene el primo operativo: cada vez que sumás, restás, multiplicás, dividís o sacás raíz, el resultado matemático casi nunca cae justo sobre la grilla, y el hardware lo redondea al double más cercano. Un error chico, sí: como mucho medio paso de grilla. Pero tu simulación hace millones de esas operaciones seguidas (Temas 42–43).

La buena noticia: ese error tiene contrato escrito (IEEE 754): acotado, predecible y casi siempre de “redondeo correcto”. Entender el contrato es lo que separa al que depura del que reza.

  • ¿Cuánto puede mentir una sola operación, en el peor caso?
  • ¿Qué significa “redondeo al par” y cuándo lo notás?
  • ¿Por qué 1/3 y 2,675 no son lo que parecen?
  • ¿Cómo mido el error sin salir de Python?

02 · Definición

El contrato: medio ulp por operación

◈

ulp

El paso de la grilla en tu zona: ulp(1,0) ≈ 2,2e−16, ulp(1e16) = 2,0. Todo error se mide en ulps, no en decimales.

⇄

Error ≤ ½ ulp

fl(a ∘ b) es el vecino más próximo al real. El redondeo es correcto: ninguna otra grilla está más cerca.

▣

Al par en el empate

Si el real cae justo a mitad de dos doubles, se elige el de mantisa par. Sin sesgo sistemático hacia arriba.

Cuánto miente una operación aislada.
Operación exactaLo que guarda el doubleError
1 / 30,3333333333333333148…≈ +0,16 ulp
0,1 escrito0,10000000000000000555…≈ +0,4 ulp
2⁵³ + 1 entero9007199254740992 (el par)1 entero = 0,5 ulp local

03 · Anatomía

Representación, operación y empate

“Redondeo” se usa para tres momentos distintos. Mezclarlos es la mitad de los bugs de punto flotante:

Redondeo de representación
Al escribir 0.1 o 2.675 ya redondeás: el literal se convierte al vecino más cercano. El error nace antes de operar. (Tema 39.)
Redondeo de operación
Cada + − * / sqrt calcula exacto por dentro (con bits extra) y redondea una sola vez al final. Error ≤ ½ ulp por operación, ni más ni menos.
Empate al par
El caso borde “justo al medio” se resuelve hacia la mantisa par: 2⁵³+1 → 2⁵³ y 2⁵³+3 → 2⁵³+4. Por eso los enteros grandes se vuelven salteados (laboratorio).

Bien redondeado

Una vez, al final

El hardware opera con precisión extendida interna y redondea solo al guardar: a*b+c con fma tiene ½ ulp, sin redondeo intermedio.

Mal redondeado

Dos veces (doble redondeo)

Pasar por float32 y luego a float64 redondea dos veces: el resultado puede caer en el vecino equivocado por 1 ulp. Precisión intermedia ≠ gratis.

04 · Ejemplos

Tres redondeos que verás en tu carrera

Valor real→Vecino más cercano→Error ≤ ½ ulp
  1. 1
    1/3 no existe.

    El real 0,333… cae entre dos doubles: Python guarda 0,3333333333333333148. Error de 0,16 ulp, invisible hasta que lo restás de sí mismo escalado.

  2. 2
    2,675 se imprime 2,67.

    El literal ya es 2,67499999999999982…: redondear a 2 decimales da 2,67 aunque en el pizarrón “debería” dar 2,68. No es bug del round: es el vecino.

  3. 3
    2⁵³+1 == 2⁵³.

    Más allá de 9007199254740992 el paso es 2: los impares no existen y redondean al par más cercano. Contadores e IDs enteros gigantes no van en float.

05 · Demostración en Python

Medir el medio ulp

Python expone la grilla: math.ulp te da el paso local y Fraction te da el valor exacto para comparar. Tres líneas y ves el contrato en vivo.

Python en tu navegador. Cambiá el 1/3 por 0,1 o 2,675 y mirá en qué vecino cae cada uno.

import math
from fractions import Fraction

x = 1 / 3
exacto = Fraction(1, 3)
print(repr(x))
print("ulp en 1/3:", math.ulp(x))
print("error en ulps:", float(Fraction(x) - exacto) / math.ulp(x))

Verás el vecino …3148 y un error de una fracción de ulp: redondeo correcto, ni más ni menos.

El par que se come los impares

base = 2 ** 53
for n in [base - 1, base, base + 1, base + 2, base + 3]:
    print(n, "->", float(n) == float(n + 0.0), int(float(n)) - n)

06 · Exploración

Laboratorio: los enteros que desaparecen

Movete alrededor de 2⁵³ = 9007199254740992: debajo todos los enteros existen; encima el paso es 2 y los impares redondean al par más cercano. Cada barra es el error |fl(N) − N|: 0 = existe, 1 = redondeado al vecino.

EXPERIMENTO 40

fl(N) frente a N

error = |fl(N) − N| ≤ 1

Los resultados numéricos aparecen debajo.
Representables—
Error máx.—
ulp local—
Veredicto—

Debajo de 2⁵³ todo existe; encima, la mitad redondea.

La curva es el error de cada entero N: 0 donde el entero existe en la grilla, 1 donde colapsa al par vecino. Pasado 2⁵³ el patrón 0-1-1-0 se repite.

Preguntas para explorar

  1. Con inicio −4 y 32 valores: ¿desde qué N el error deja de ser 0? ¿Qué tiene de especial ese N?
  2. Subí el inicio a +40: ¿qué fracción de enteros es representable? ¿Qué pasa con N+1 == N?
  3. Si un ID o un reloj de simulación vive en float pasado 2⁵³, ¿qué eventos se pisan?
Ver respuestas sugeridas
  1. En 2⁵³: hasta 2⁵³−1 todo existe (ulp = 1); desde 2⁵³ el paso es 2 y los impares redondean (½ ulp local = 1 entero).
  2. Solo la mitad (~50 %): los impares colapsan al par. float(N+1) == float(N) es True para N impar ≥ 2⁵³: el +1 desaparece.
  3. Dos eventos con IDs o tiempos consecutivos caen en el mismo double: el orden se pierde y los == mienten. Relojes e IDs van en enteros (Tema 39).

07 · Comprensión

Confusiones frecuentes

«Redondeo y truncamiento son lo mismo»

No: redondear elige el vecino más cercano (error ≤ ½ ulp); truncar corta hacia cero (error hasta 1 ulp y con sesgo). El hardware redondea; truncás vos al convertir a int. Lo separamos en el Tema 41.

«El error es aleatorio y se cancela solo»

El redondeo es determinista y a veces sistemático (siempre hacia el mismo vecino). Que se cancele hay que demostrarlo con análisis, no suponerlo: la suma de mil redondeos chicos puede derivar (Tema 42).

«Con más dígitos impresos hay más precisión»

Imprimir 20 decimales no crea información: pasado el ulp 15–16, el resto son dígitos del vecino, no del valor real. Precisión la da la grilla, no el print.

«Si el error es ½ ulp, nunca importa»

Una operación no; un millón sí, y una resta de casi-iguales lo amplifica brutalmente (cancelación, Tema 43). El ½ ulp es el ladrillo: la pared la construye tu algoritmo.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: medir tu primer ulp

Mostrá que el paso en 1,0 es ε y que 1/3 vive a una fracción de ulp del real.

import math
from fractions import Fraction

print(math.ulp(1.0))
err = float(Fraction(1 / 3) - Fraction(1, 3))
print(repr(1 / 3), err / math.ulp(1 / 3))
Ver solución razonada

ulp(1,0) ≈ 2,22e−16 y el cociente da ≈ +0,16: el vecino correcto por encima del real. Si te da negativo cerca de −0,5…+0,5, sigue siendo redondeo correcto: el signo dice de qué lado cayó.

Ejercicio 2: el impar que no existe

Verificá que 2⁵³+1 colapsa al par y que 2⁵³+3 salta al par siguiente.

base = 2 ** 53
print(int(float(base + 1)) - (base + 1))
print(int(float(base + 3)) - (base + 3))
print(float(base + 1) == float(base))
Ver solución

−1 +1 True: el +1 redondea hacia abajo (al par base) y el +3 hacia arriba (al par base+4). Empate al par en acción, no capricho.

Ejercicio 3: redondear a mano

Escribí vecinos(x) que devuelva los dos doubles adyacentes y el error de x en ulps usando Fraction como verdad.

Ver una posible respuesta
import math
from fractions import Fraction

def vecinos(x, num=1, den=10):
    exacto = Fraction(num, den)
    u = math.ulp(float(x))
    err_ulp = float(Fraction(float(x)) - exacto) / u
    abajo = math.nextafter(float(x), float("-inf"))
    arriba = math.nextafter(float(x), float("inf"))
    return abajo, float(x), arriba, err_ulp


print(vecinos(0.1))

Verás los dos vecinos de 0,1 y un error ≈ +0,4 ulp: el literal ya nació redondeado antes de tu primera operación.

09 · Síntesis

Ideas para recordar

  • Cada operación fl() redondea al vecino más cercano: error ≤ ½ ulp.
  • El error relativo por operación está acotado por ε/2 ≈ 1,1e−16.
  • El empate se resuelve al par: sin sesgo hacia arriba.
  • Redondear ≠ truncar: el hardware redondea, el int() trunca.
  • Medí con ulp y Fraction; IDs y relojes grandes van en enteros.

En el próximo tema, el hermano sesgado del redondeo: el error de truncamiento, cuando cortamos decimales a propósito.