Este código de ejemplo ilustra que std::rand
es un caso de legado culto al cargo que debería hacer que sus cejas se eleven cada vez que lo vea.
Hay varios problemas aqui:
El contrato que la gente suele asumir, incluso las pobres almas desventuradas que no saben nada mejor y no pensarán en ello precisamente en estos términos, es que las rand
muestras de la distribución uniforme de los números enteros en 0, 1, 2,… RAND_MAX
,, y cada llamada produce una muestra independiente .
El primer problema es que el contrato asumido, muestras aleatorias uniformes e independientes en cada llamada, no es realmente lo que dice la documentación y, en la práctica, históricamente las implementaciones no lograron proporcionar ni el más mínimo simulacro de independencia. Por ejemplo, C99 §7.20.2.1 'La rand
función' dice, sin más detalles:
La rand
función calcula una secuencia de enteros pseudoaleatorios en el rango de 0 a RAND_MAX
.
Esta es una oración sin sentido, porque la pseudoaleatoriedad es una propiedad de una función (o familia de funciones ), no de un número entero, pero eso no impide que incluso los burócratas de ISO abusen del lenguaje. Después de todo, los únicos lectores a los que les molestaría saber que no deben leer la documentación rand
por temor a que sus células cerebrales se descompongan.
Una implementación histórica típica en C funciona así:
static unsigned int seed = 1;
static void
srand(unsigned int s)
{
seed = s;
}
static unsigned int
rand(void)
{
seed = (seed*1103515245 + 12345) % ((unsigned long)RAND_MAX + 1);
return (int)seed;
}
Esto tiene la desafortunada propiedad de que , aunque una sola muestra puede distribuirse uniformemente bajo una semilla aleatoria uniforme (que depende del valor específico de RAND_MAX
), alterna entre enteros pares e impares en llamadas consecutivas, después de
int a = rand();
int b = rand();
la expresión (a & 1) ^ (b & 1)
da 1 con 100% de probabilidad, lo que no es el caso de muestras aleatorias independientes en cualquier distribución compatible con enteros pares e impares. Por lo tanto, surgió un culto de carga en el que uno debería descartar los bits de bajo orden para perseguir a la escurridiza bestia de la "mejor aleatoriedad". (Alerta de spoiler: este no es un término técnico. Es una señal de que la prosa que estás leyendo no sabe de lo que están hablando o piensa que no tienes ni idea y debes ser condescendiente).
El segundo problema es que incluso si cada llamada muestreó independientemente de una distribución aleatoria uniforme en 0, 1, 2,… RAND_MAX
, el resultado derand() % 6
no se distribuiría uniformemente en 0, 1, 2, 3, 4, 5 como un dado. tirar, a menos que RAND_MAX
sea congruente con -1 módulo 6. Contraejemplo simple: si RAND_MAX
= 6, entonces desde rand()
, todos los resultados tienen la misma probabilidad 1/7, pero desde rand() % 6
, el resultado 0 tiene probabilidad 2/7 mientras que todos los demás resultados tienen probabilidad 1/7 .
La forma correcta de hacer esto es con muestreo de rechazo: extraer repetidamente una muestra aleatoria uniforme e independiente s
de 0, 1, 2,… RAND_MAX
, y rechace (por ejemplo) los resultados 0, 1, 2,…, ((RAND_MAX + 1) % 6) - 1
—si obtiene uno de esos, empezar de nuevo; de lo contrario, cede s % 6
.
unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
continue;
return s % 6;
De esta manera, el conjunto de resultados rand()
que aceptamos es divisible por 6, y cada resultado posible de s % 6
se obtiene por el mismo número de resultados aceptados de rand()
, por lo que si rand()
se distribuye uniformemente, entonces también lo ess
. No hay límite en el número de ensayos, pero el número esperado es menor que 2 y la probabilidad de éxito aumenta exponencialmente con el número de ensayos.
La elección de cuál los resultados de la rand()
rechaza es irrelevante, siempre y cuando se asigna el mismo número de ellos a cada número entero inferior a 6. El código en cppreference.com hace una diferente opción, debido al primer problema por encima de la que no se garantiza nada acerca de la distribución o independencia de salidas de rand()
, y en la práctica, los bits de bajo orden exhibieron patrones que no "parecen lo suficientemente aleatorios" (no importa que la siguiente salida sea una función determinista de la anterior).
Ejercicio para el lector: Demostrar que el código en cppreference.com produce una distribución uniforme sobre rodillos de matriz si rand()
los rendimientos de una distribución uniforme en 0, 1, 2, ..., RAND_MAX
.
Ejercicio para el lector: ¿Por qué preferiría rechazar uno u otro subconjunto? ¿Qué cálculo se necesita para cada ensayo en los dos casos?
Un tercer problema es que el espacio semilla es tan pequeño que incluso si la semilla se distribuye uniformemente, un adversario armado con el conocimiento de su programa y un resultado, pero no la semilla, puede predecir fácilmente la semilla y los resultados subsiguientes, lo que hace que no parezcan tan al azar después de todo. Así que ni siquiera pienses en usar esto para criptografía.
Puede seguir la elegante ruta de ingeniería excesiva y la std::uniform_int_distribution
clase de C ++ 11 con un dispositivo aleatorio apropiado y su motor aleatorio favorito, como el siempre popular tornado Mersenne, std::mt19937
para jugar a los dados con su primo de cuatro años, pero incluso eso no va a funcionar. estar en forma para generar material de claves criptográficas, y el tornado de Mersenne también es un terrible acaparador de espacio con un estado de varios kilobytes que causa estragos en la memoria caché de su CPU con un tiempo de configuración obsceno, por lo que es malo incluso para, por ejemplo , simulaciones de Monte Carlo en paralelo con árboles reproducibles de subcomputaciones; su popularidad probablemente se deba principalmente a su pegadizo nombre. ¡Pero puedes usarlo para tirar dados de juguete como este ejemplo!
Otro enfoque es usar un generador de números pseudoaleatorios criptográfico simple con un estado pequeño, como un simple borrado rápido de clave PRNG , o simplemente un cifrado de flujo como AES-CTR o ChaCha20 si está seguro ( por ejemplo , en una simulación de Monte Carlo para investigación en ciencias naturales) que no hay consecuencias adversas para predecir resultados pasados si el estado alguna vez se ve comprometido.
std::uniform_int_distribution
para los dados