Questo codice di esempio illustra che std::rand
è un caso di balderdash di culto del carico legacy che dovrebbe farti sollevare le sopracciglia ogni volta che lo vedi.
Ci sono diversi problemi qui:
Il contratto che le persone di solito assumono - anche le povere anime sfortunate che non sanno niente di meglio e non ci penseranno esattamente in questi termini - è che rand
campioni dalla distribuzione uniforme sugli interi in 0, 1, 2, ... RAND_MAX
, e ogni chiamata produce un campione indipendente .
Il primo problema è che il contratto assunto, campioni casuali uniformi indipendenti in ogni chiamata, non è effettivamente ciò che dice la documentazione e, in pratica, le implementazioni storicamente non sono riuscite a fornire nemmeno il più semplice simulacro di indipendenza. Ad esempio, C99 §7.20.2.1 'La rand
funzione' dice, senza elaborazione:
La rand
funzione calcola una sequenza di numeri interi pseudo-casuali nell'intervallo da 0 a RAND_MAX
.
Questa è una frase priva di significato, perché la pseudocasualità è una proprietà di una funzione (o famiglia di funzioni ), non di un numero intero, ma ciò non impedisce nemmeno ai burocrati ISO di abusare del linguaggio. Dopotutto, gli unici lettori che ne sarebbero sconvolti sanno meglio che leggere la documentazione rand
per paura che le loro cellule cerebrali si deteriorino.
Una tipica implementazione storica in C funziona in questo modo:
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;
}
Questo ha la sfortunata proprietà che anche se un singolo campione può essere distribuito uniformemente sotto un seme casuale uniforme (che dipende dal valore specifico di RAND_MAX
), alterna interi pari e dispari in chiamate consecutive, dopo
int a = rand();
int b = rand();
l'espressione (a & 1) ^ (b & 1)
restituisce 1 con probabilità del 100%, il che non è il caso di campioni casuali indipendenti su qualsiasi distribuzione supportata su numeri interi pari e dispari. Così, è emerso un culto del carico che si dovrebbe scartare i pezzi di basso ordine per inseguire la bestia sfuggente della "migliore casualità". (Avviso spoiler: questo non è un termine tecnico. Questo è un segno che la prosa di chi stai leggendo o non sa di cosa stanno parlando, o pensa che tu non abbia idea e debba essere condiscendente.)
Il secondo problema è che anche se ogni chiamata campionasse indipendentemente da una distribuzione casuale uniforme su 0, 1, 2, ... RAND_MAX
, il risultato di rand() % 6
non sarebbe distribuito uniformemente in 0, 1, 2, 3, 4, 5 come un dado roll, a meno che non RAND_MAX
sia congruente a -1 modulo 6. Semplice controesempio: se RAND_MAX
= 6, allora da rand()
, tutti i risultati hanno probabilità uguale 1/7, ma da rand() % 6
, il risultato 0 ha probabilità 2/7 mentre tutti gli altri risultati hanno probabilità 1/7 .
Il modo giusto per farlo è con il campionamento del rifiuto: disegna ripetutamente un campione casuale uniforme indipendente s
da 0, 1, 2, ... RAND_MAX
, e rifiuta (ad esempio) i risultati 0, 1, 2, ..., ((RAND_MAX + 1) % 6) - 1
- se ottieni uno dei quelli, ricomincia; altrimenti, cedere s % 6
.
unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
continue;
return s % 6;
In questo modo, l'insieme di risultati da rand()
quello che accettiamo è uniformemente divisibile per 6, e ogni possibile risultato da s % 6
è ottenuto dallo stesso numero di risultati accettati da rand()
, quindi se rand()
è uniformemente distribuito, lo è s
. Non vi è alcun limite al numero di prove, ma il numero atteso è inferiore a 2 e la probabilità di successo cresce esponenzialmente con il numero di prove.
La scelta di quali risultati rand()
rifiutare è irrilevante, a condizione di mappare un numero uguale di essi a ciascun numero intero inferiore a 6. Il codice su cppreference.com fa una scelta diversa , a causa del primo problema sopra, che nulla è garantito sul distribuzione o indipendenza degli output dirand()
, e in pratica i bit di ordine inferiore hanno mostrato schemi che non "sembrano abbastanza casuali" (non importa che l'output successivo sia una funzione deterministica di quello precedente).
Esercizio per il lettore: dimostra che il codice su cppreference.com produce una distribuzione uniforme sui rotoli di dado se rand()
produce una distribuzione uniforme su 0, 1, 2, ...,RAND_MAX
.
Esercizio per il lettore: perché potresti preferire che uno o gli altri sottoinsiemi rifiutino? Quale calcolo è necessario per ogni prova nei due casi?
Un terzo problema è che lo spazio seme è così piccolo che anche se il seme è distribuito uniformemente, un avversario armato della conoscenza del tuo programma e di un risultato ma non il seme può prontamente prevedere il seme e i risultati successivi, il che li fa sembrare non così dopo tutto casuale. Quindi non pensare nemmeno di usarlo per la crittografia.
Puoi seguire la stravagante strada ingegnerizzata e la std::uniform_int_distribution
classe C ++ 11 con un dispositivo casuale appropriato e il tuo motore casuale preferito come il sempre popolare tornado Mersenne std::mt19937
per giocare ai dadi con tuo cugino di quattro anni, ma anche questo non lo farà essere adatto a generare materiale per chiavi crittografiche - e anche il Mersenne Twister è un terribile maiale spaziale con uno stato multi-kilobyte che crea scompiglio nella cache della CPU con un tempo di configurazione osceno, quindi è dannoso anche per, ad es. , simulazioni Monte Carlo parallele con alberi riproducibili di sottocomputer; la sua popolarità deriva probabilmente principalmente dal suo nome accattivante. Ma puoi usarlo per lanciare dadi giocattolo come questo esempio!
Un altro approccio consiste nell'utilizzare un semplice generatore di numeri pseudocasuali crittografici con uno stato piccolo, come una semplice cancellazione rapida della chiave PRNG , o semplicemente un cifrario a flusso come AES-CTR o ChaCha20 se sei sicuro ( ad esempio , in una simulazione Monte Carlo per ricerca nelle scienze naturali) che non ci sono conseguenze negative nel prevedere i risultati passati se lo stato è mai compromesso.
std::uniform_int_distribution
per i dadi