01 · Punto de partida
Medio ulp × un millón
En los Temas 39–41 viste el error de una operación: como mucho ½ ulp si redondeás (Tema 40), hasta 1 unidad sesgada si truncás (Tema 41). El problema es que una simulación no hace una operación: avanza un reloj con t += dt millones de veces, acumula energía, promedia réplicas, suma histogramas. Cada paso agrega su redondeo al pozo común.
Y el pozo tiene formas de crecer: si los redondeos tiran siempre del mismo lado (truncamiento, pasos todos positivos), el error crece lineal con N; si tiran al azar, crece como √N; y si sumás chicos sobre un gigante, el error es todo: los chicos desaparecen por absorción (Tema 39).
- ¿Cuánto error junto al sumar N veces el mismo paso?
- ¿Por qué
1e16 + 1 + 1 + …queda clavado en 1e16? - ¿Por qué sumar chicos-primero cambia el resultado?
- ¿Cómo sumo largo sin pagar el sesgo: bloques, Kahan, fsum?
02 · Definición
Tres velocidades del desastre
Lineal (sesgo)
Pasos todos del mismo signo o truncados: cada redondeo suma. Error ≈ N × ½ ulp. Es el reloj t += 0,1.
Raíz (azar)
Redondeos que a veces suman y a veces restan: se compensan a medias. Error ≈ √N × ½ ulp. Mejor, pero igual crece.
Absorción (todo)
Chico frente a gigante: grande + chico == grande. Error = 100 % del aporte chico, desde el primer paso.
| Suma de N términos | Error típico | Ejemplo |
|---|---|---|
| N pasos +0,1 ingenuos | Lineal, deriva visible | Reloj que se atrasa/adelanta |
| N valores mixtos ± | √N, paseo aleatorio | Promedio con signos mezclados |
| 1e16 + N unos | Total: N perdidos | Contador gigante + eventos chicos |
03 · Anatomía
Por qué el orden cambia la suma
La suma matemática es asociativa; la suma en float no: (a+b)+c ≠ a+(b+c) en general. Cada paréntesis redondea, y redondear temprano sobre un gigante mata a los chicos antes de que se junten entre ellos.
- Grande-primero (pierde)
((G + 1) + 1) + …: cada+1muere solo contra G por absorción. Al final queda G, aunque N valga miles.- Chicos-primero (salva)
G + (1 + 1 + …): los chicos se juntan entre ellos hasta ser grandes frente a G y recién ahí se suman. Se pierde solo el redondeo final.- Suma compensada (Kahan / fsum)
- Guarda aparte “lo que se perdió” en cada paso y lo reinyecta en el siguiente: error casi independiente de N.
math.fsumes la versión exacta-y-lenta; Kahan, la rápida.
Bien acumulado
Chicos primero o Kahan
Ordenar ascendente, sumar por bloques o usar fsum/Kahan: el error queda en 1–2 ulps aunque N sea enorme.
Mal acumulado
Naive sobre el gigante
total = 1e16; for v in chicos: total += v: cada aporte muere en ½ ulp contra el gigante. Más iteraciones, cero progreso.
04 · Ejemplos
Tres acumulaciones reales
- 1El reloj que se corre.
t = 0; for _ in range(10_000): t += 0,1termina corrido ~1e−12: cada+= 0,1redondea y el sesgo camina. Con enteros (t = i*dt) no deriva. - 2El contador clavado.
1e16 + 1 + 1 + …(1000 unos) queda en1e16: el paso local es 2 y cada uno muere solo. Sumando los unos primero da10000000000001000. - 3El promedio mentiroso.
Promediar 1 M de energías con un outlier gigante ingenuo arrastra el error de la suma al cociente. Con
fsumo promedio incremental (Welford, Tema 43) el error no escala con N.
05 · Demostración en Python
Perder mil unos en una línea
El experimento mínimo: un gigante y mil unos. La suma ingenua los pierde todos; fsum y Kahan los recuperan.
Python en tu navegador. Subí la lista a 5000 unos y mirá cómo la ingenua sigue clavada mientras las honestas avanzan.
import math
vals = [1e16] + [1.0] * 1000
print("naive:", sum(vals))
print("fsum: ", math.fsum(vals))
print("al revés fsum:", math.fsum(reversed(vals)))
Naive ≈ 1e16 clavado; fsum ≈ 10000000000001000: los mil unos reaparecen.
Kahan en 6 líneas
def kahan(vals):
total = comp = 0.0
for v in vals:
y = v - comp
t = total + y
comp = (t - total) - y
total = t
return total
print(kahan([1e16] + [1.0] * 1000))
06 · Exploración
Laboratorio: el gigante que se come los unos
Gigante fijo G = 1e16 más N unos. La curva naranja suma ingenuo de izquierda a derecha (((G+1)+1)…) y queda clavada; la verde junta los unos primero (G + N) y crece. Movelo y contá cuántos unos se pierden.
G + N unos, dos órdenes
perdidos = N − (naive − G)
Con N = 1000 la ingenua pierde los 1000 unos.
El eje x es cuántos unos ya se sumaron; el eje y es cuánto creció el total sobre G. La ingenua queda en 0; la honesta sube en diagonal.
Preguntas para explorar
- Con N = 1000: ¿cuánto vale la ingenua menos G? ¿Y la chicos-primero menos G?
- Subí a N = 5000: ¿la ingenua recupera algo? ¿Por qué el paso local (ulp = 2) lo impide?
- Si G fuera tu energía acumulada y los unos tus eventos, ¿qué orden elegirías para el total?
Ver respuestas sugeridas
- Ingenua: 0 (los 1000 mueren uno por uno por absorción); chicos-primero: 1000 (los unos se juntan hasta 1000 y recién ahí enfrentan a G).
- No: cada
+1aislado es menor queG·ε/2 = 1,0…redondeado al par de paso 2. Solo un bloque ≥ 2 sobrevive solo; el resto necesita juntarse primero. - Chicos-primero, por bloques o Kahan: acumular parciales chicos y sumarlos al gigante al final. Es el patrón de
fsumy del Tema 43.
07 · Comprensión
Confusiones frecuentes
«Sumar es asociativo, el orden da igual»
En matemática sí; en float cada paréntesis redondea. (G+1)+1 pierde y G+(1+1) salva: el orden es algoritmo.
«Con N grande el error se promedia a cero»
Solo si los redondeos son simétricos. Con pasos todos positivos o truncados el sesgo es lineal con N: crece, no se cancela (Tema 41).
«fsum/Kahan son para obsesivos»
Son para totales largos o dispares: pasar de error ×N a error ×1 con unas líneas. fsum es la verdad de referencia para medir.
«Si cada paso tiene ½ ulp, el total está bien»
½ ulp × 10⁶ pasos = 5×10⁵ ulps de cota: en 1,0 eso es ~1e−10, y con absorción es el 100 %. La cota por operación no es cota del total.
08 · Práctica guiada
Ejercicios con Python
Ejercicio 1: el reloj que deriva
Compará t += 0,1 N veces contra i*0,1 y medí la deriva.
N = 10000
t = 0.0
for _ in range(N):
t += 0.1
print(repr(t), repr(N * 0.1), repr(t - N * 0.1))
Ver solución razonada
Deriva ≈ 1e−12 creciendo con N: cada += redondea con sesgo. El reloj honesto es entero (i) escalado al final (i*dt).
Ejercicio 2: contar los perdidos
Medí cuántos unos pierde la suma ingenua sobre 1e16 con N = 2000.
import math
G, N = 1e16, 2000
naive = G
for _ in range(N):
naive += 1.0
print("naive - G:", naive - G)
print("fsum - G:", math.fsum([G] + [1.0] * N) - G)
Ver solución
0,0 vs 2000,0: la ingenua pierde los 2000 por absorción (ulp = 2 en G); fsum los recupera juntando exacto por dentro.
Ejercicio 3: ordenar salva
Escribí suma_ordenada (ascendente por |x|) y comparala con sum sobre valores dispares.
Ver una posible respuesta
import math
def suma_ordenada(vals):
return math.fsum(sorted(vals, key=abs))
vals = [1e16, 1.0, 1.0, 1.0, -1e16]
print(sum(vals), suma_ordenada(vals), math.fsum(vals))
La ingenua puede dar 2,0 o 0,0 según el orden de llegada; la ordenada y fsum dan 3,0 estable: chicos con chicos primero.
09 · Síntesis
Ideas para recordar
- N sumas ingenuas: cota N × ½ ulp; con sesgo crece lineal, al azar como √N.
- Absorción: chico + gigante = gigante; el aporte se pierde entero.
- La suma float no es asociativa: chicos-primero, bloques o Kahan.
- Relojes con enteros (
i*dt); totales críticos confsum. - El promedio hereda el error de la suma: primero sumar bien, después dividir.
En el próximo tema convertimos estas curas en método: la estabilidad numérica, o cómo elegir algoritmos que no amplifiquen el error.