Números Pseudoaleatorios · Tema 16

Generadores basados en desplazamientos de bits

Velocidad pura con XOR y shifts: cómo el xorshift32 mezcla bits, por qué el cero lo mata y por qué en Python hay que enmascarar a 32 bits.

01 · Punto de partida

Adiós multiplicación, hola bits

Los LCG viven de a·x + c y un mod caro. En 2003 Marsaglia mostró otra vía: solo XOR y desplazamientos (<<, >>), las operaciones más baratas de la CPU. Tres líneas, sin multiplicar ni dividir, período 2³²−1 y velocidad endemoniada.

La idea es vieja (registros LFSR de hardware) pero el envase es nuevo: el xorshift32. Entenderlo te abre la puerta a todos los modernos (xoshiro, PCG usa parte de esto).

  • ¿Qué hacen ^, << y >> a los bits?
  • ¿Por qué tres xorshifts encadenados mezclan y uno solo no?
  • ¿Por qué la semilla 0 es letal aquí también?
  • ¿Por qué en Python hay que enmascarar con 0xFFFFFFFF?

02 · Definición

XOR, shifts y el trío mágico

◈

XOR ^

Mezcla reversible bit a bit: 1^1=0, 1^0=1. Es lo que difunde un cambio de un bit a muchos.

⇄

Shifts

x<<k mueve bits a la izquierda (entra 0); x>>k a la derecha. Solos pierden info; con XOR la propagan.

▣

Trío (13,17,5)

Izquierda-derecha-izquierda con esos corrimientos da período 2³²−1. No cualquier trío sirve: solo una lista validada (Marsaglia).

Xorshift32 frente a LCG.
AspectoLCGXorshift32
Operacionesmult + suma + modxor + shifts
Período≤ m2³²−1 (trío válido)
Semilla 0Mata al multiplicativo; mixto sobreviveMata a todos: 0→0→0
EstadoUn enteroUn entero de 32 bits

03 · Trampa de Python

Enmascarar o morir (en la portabilidad)

En C, uint32_t trunca solo a 32 bits. En Python los enteros son infinitos: x << 13 crece sin cortar y tu secuencia deja de ser xorshift32. La solución es enmascarar tras cada paso:

Máscara
x &= 0xFFFFFFFF tras cada XOR-shift: simula el desborde de 32 bits.
Cero
Si x == 0, el siguiente es 0: validá semilla ≠ 0 al sembrar.
Salida
u = x / 2**32 en [0,1). Nunca x % m con otro m sin pensar (Tema 22).

Bien portado

32 bits siempre

Máscara en cada paso, período y secuencia idénticos a C. Reproducible entre lenguajes.

Sin máscara

Entero infinito

Funciona en Python pero es otro generador: no coincide con C ni tiene el período prometido.

04 · Ejemplos

Un bit cambia todo

Semilla ≠ 0→3 xorshifts→u = x/2³²
  1. 1
    Semilla 12345.

    Cascada de bits que en 3 pasos ya es irreconocible: difusión total.

  2. 2
    Semilla 12346 (un bit más).

    Historia totalmente distinta desde el primer valor: sensibilidad extrema.

  3. 3
    Semilla 0.

    0^0 = 0 tres veces: secuencia muerta. Validar ≠ 0 es obligatorio.

05 · Implementación en Python

Xorshift32 fiel a C

Python en tu navegador. Quitá una máscara y observá cómo diverge de la referencia.

MASK = 0xFFFFFFFF

def xorshift32(semilla, n=6):
    assert semilla != 0, "semilla 0 prohibida"
    x = semilla & MASK
    sal = []
    for _ in range(n):
        x ^= (x << 13) & MASK
        x &= MASK
        x ^= x >> 17
        x ^= (x << 5) & MASK
        x &= MASK
        sal.append(x / 2**32)
    return sal


print([round(v, 6) for v in xorshift32(12345)])

Seis uniformes irreconocibles desde 12345. Con 12346 sale otra historia desde el valor 1.

El cero mata

def paso(x):
    x ^= (x << 13) & 0xFFFFFFFF
    x &= 0xFFFFFFFF
    x ^= x >> 17
    x ^= (x << 5) & 0xFFFFFFFF
    return x & 0xFFFFFFFF


print(paso(0))

06 · Exploración

Laboratorio: bits que mezclan

Elegí variante y semilla (incluido 0 para ver la muerte). La curva es u(n); el panel muestra los primeros valores, distintos y alerta de cero. Compará xorshift con el LCG didáctico.

EXPERIMENTO 16

Mezcla en 3 pasos

13 · 17 · 5 vs. LCG

Los resultados numéricos aparecen debajo.
Primer u—
Distintos—
¿Muerto en 0?—
Lectura—

Con semilla 12345 el xorshift cubre sin orden visible.

Línea plana en cero = semilla prohibida. Todo lo demás debe cubrir sin ciclos visibles en esta ventana.

Preguntas para explorar

  1. Poné semilla 0 en xorshift: ¿qué sale? ¿Y en LCG mixto con semilla 0?
  2. Cambiá de 12345 a 12346 en xorshift: ¿se parecen las historias? ¿Qué dice de la difusión?
  3. Si olvidaras la máscara en Python, ¿seguiría dando en [0,1)? ¿Sería el mismo generador?
Ver respuestas sugeridas
  1. Xorshift: todo cero (muerto). LCG mixto: sobrevive por el +3. Distinta familia, distinta trampa.
  2. No se parecen en nada desde el valor 1: un bit cambia la cascada entera. Esa es la difusión buscada.
  3. Podría salirse de rango o divergir de C: sería otro algoritmo con otro período. La máscara es especificación.

07 · Comprensión

Confusiones frecuentes

«Cualquier trío de shifts sirve»

Solo los validados dan período máximo y buenas propiedades. Un trío improvisado puede ciclar corto o mezclar pobre.

«En Python no necesito máscara»

La necesitas para ser xorshift32 de verdad y coincidir con C. Sin ella es otro bicho con enteros infinitos.

«Xorshift es criptográfico por ser de bits»

No: es lineal y predecible con pocas observaciones. Para seguridad se exige otro diseño (Tema 18).

«Shifts a la derecha con signo dan igual»

En lenguajes con signo, >> puede replicar el bit de signo. Se opera en sin signo de 32 bits.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: un paso a mano

Partiendo de x = 1, aplicá los 3 xorshifts con máscara y verificá que da 270369.

x = 1
x ^= (x << 13) & 0xFFFFFFFF
x &= 0xFFFFFFFF
x ^= x >> 17
x ^= (x << 5) & 0xFFFFFFFF
x &= 0xFFFFFFFF
print(x)
Ver solución razonada

Debe dar 270369 (valor clásico de referencia). Si te da otro, revisá máscaras intermedias, no solo la final.

Ejercicio 2: el cero

Demostrá que el 0 es punto fijo absorbente para cualquier trío.

def paso(x, a=13, b=17, c=5):
    x ^= (x << a) & 0xFFFFFFFF
    x &= 0xFFFFFFFF
    x ^= x >> b
    x ^= (x << c) & 0xFFFFFFFF
    return x & 0xFFFFFFFF


print(paso(0))
Ver solución

Da 0 siempre: 0^0=0 y shifts de 0 son 0. Por eso el estado nunca debe ser todo ceros (en versiones mult palabra, ninguna palabra toda cero según diseño).

Ejercicio 3: sensibilidad

Compará las primeras 5 salidas desde 12345 y 12346. ¿Cuántas coinciden?

Ver una posible respuesta
def xs(s, n=5):
    x = s
    sal = []
    for _ in range(n):
        x ^= (x << 13) & 0xFFFFFFFF
        x &= 0xFFFFFFFF
        x ^= x >> 17
        x ^= (x << 5) & 0xFFFFFFFF
        x &= 0xFFFFFFFF
        sal.append(round(x / 2**32, 6))
    return sal


print(xs(12345))
print(xs(12346))

Ninguna (o casi): la difusión es total desde el primer paso. Es lo que lo hace útil y a la vez impredecible a ojo.

09 · Síntesis

Ideas para recordar

  • Xorshift32: 3 XOR-shifts (13,17,5) en 32 bits, u = x/2³².
  • Rápido y período 2³²−1 con trío válido; cero prohibido.
  • En Python, máscara en cada paso para ser fiel a C.
  • Crudo es bueno pero lineal: los modernos agregan scrambling.
  • No es criptográfico aunque sea «de bits».

En el próximo tema veremos el estado del arte: generadores modernos (Mersenne Twister, PCG, xoshiro, Philox).