Cet exemple de code illustre qu'il std::rand
s'agit d'un cas de balderdash culte du fret hérité qui devrait faire lever vos sourcils à chaque fois que vous le voyez.
Il y a plusieurs problèmes ici:
Les gens contractuels supposent généralement - même les pauvres âmes malheureuses qui ne savent pas mieux et ne penseront pas à cela précisément en ces termes - est que des rand
échantillons de la distribution uniforme sur les entiers en 0, 1, 2,… RAND_MAX
,, et chaque appel donne un échantillon indépendant .
Le premier problème est que le contrat supposé, des échantillons aléatoires uniformes indépendants dans chaque appel, n'est pas réellement ce que dit la documentation - et dans la pratique, les implémentations ont historiquement échoué à fournir même le plus simple simulacre d'indépendance. Par exemple, C99 §7.20.2.1 'La rand
fonction' dit, sans élaboration:
La rand
fonction calcule une séquence d'entiers pseudo-aléatoires compris entre 0 et RAND_MAX
.
C'est une phrase dénuée de sens, car la pseudo-aléatoire est une propriété d'une fonction (ou d'une famille de fonctions ), pas d'un entier, mais cela n'empêche pas même les bureaucrates de l'ISO d'abuser du langage. Après tout, les seuls lecteurs qui en seraient contrariés savent mieux que de lire la documentation rand
par crainte de voir leurs cellules cérébrales se décomposer.
Une implémentation historique typique en C fonctionne comme ceci:
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;
}
Cela a la propriété malheureuse que même si un seul échantillon peut être uniformément distribué sous une graine aléatoire uniforme (qui dépend de la valeur spécifique de RAND_MAX
), il alterne entre les entiers pairs et impairs dans les appels consécutifs - après
int a = rand();
int b = rand();
l'expression (a & 1) ^ (b & 1)
donne 1 avec une probabilité de 100%, ce qui n'est pas le cas pour les échantillons aléatoires indépendants sur toute distribution prise en charge sur des entiers pairs et impairs. Ainsi, un culte de la cargaison a émergé selon lequel il fallait se débarrasser des bits de poids faible pour chasser la bête insaisissable du «meilleur hasard». (Alerte spoiler: ce n'est pas un terme technique. Ceci est un signe que la prose que vous lisez ne sait pas de quoi elle parle, ou pense que vous n'avez aucune idée et doit être condescendante.)
Le deuxième problème est que même si chaque appel échantillonnait indépendamment d'une distribution aléatoire uniforme sur 0, 1, 2,… RAND_MAX
, le résultat de rand() % 6
ne serait pas uniformément distribué en 0, 1, 2, 3, 4, 5 comme un dé rouler, sauf si elle RAND_MAX
est congruente à -1 modulo 6. Contre-exemple simple: SiRAND_MAX
= 6, alors à partir de rand()
, tous les résultats ont une probabilité égale de 1/7, mais à partir de rand() % 6
, le résultat 0 a une probabilité de 2/7 tandis que tous les autres résultats ont une probabilité de 1/7 .
La bonne façon de procéder consiste à utiliser un échantillonnage de rejet: tirez à plusieurs reprises un échantillon aléatoire uniforme indépendant s
de 0, 1, 2,… RAND_MAX
, et rejetez (par exemple) les résultats 0, 1, 2,…, ((RAND_MAX + 1) % 6) - 1
- si vous obtenez l'un des ceux-là, recommencer; sinon, cédez s % 6
.
unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
continue;
return s % 6;
De cette façon, l'ensemble des résultats rand()
que nous acceptons est divisible par 6, et chaque résultat possible s % 6
est obtenu par le même nombre de résultats acceptésrand()
, donc si rand()
est uniformément distribué, il en est de même s
. Il n'y a pas de limite sur le nombre d'essais, mais le nombre attendu est inférieur à 2 et la probabilité de succès augmente de façon exponentielle avec le nombre d'essais.
Le choix dont les résultats de rand()
vous rejetez est sans importance, à condition que vous associez un nombre égal d'entre eux à chaque entier inférieur à 6. Le code à cppreference.com fait un autre choix, en raison du premier problème ci-dessus que rien est garanti sur le la distribution ou l'indépendance des sorties de rand()
, et en pratique, les bits de poids faible présentaient des modèles qui ne «semblaient pas assez aléatoires» (sans oublier que la sortie suivante est une fonction déterministe de la précédente).
Exercice pour le lecteur: Démontrer que le code à cppreference.com produit une distribution uniforme sur les rouleaux de matrice se rand()
produit une distribution uniforme sur 0, 1, 2, ..., RAND_MAX
.
Exercice pour le lecteur: Pourquoi préféreriez-vous que l'un ou l'autre sous-ensemble soit rejeté? Quel calcul est nécessaire pour chaque essai dans les deux cas?
Un troisième problème est que l'espace de départ est si petit que même si la graine est uniformément distribuée, un adversaire armé de la connaissance de votre programme et d'un résultat, mais pas de la graine, peut facilement prédire la graine et les résultats ultérieurs, ce qui les fait paraître non. aléatoire après tout. Alors ne pensez même pas à l'utiliser pour la cryptographie.
Vous pouvez emprunter la voie sophistiquée et la std::uniform_int_distribution
classe C ++ 11 avec un appareil aléatoire approprié et votre moteur aléatoire préféré comme le toujours populaire Mersenne Twister std::mt19937
pour jouer aux dés avec votre cousin de quatre ans, mais même cela ne va pas être apte à générer du matériel de clé cryptographique - et le twister de Mersenne est également un espace terrible avec un état de plusieurs kilo-octets qui ravage le cache de votre processeur avec un temps de configuration obscène, il est donc mauvais, même pour, par exemple , des simulations de Monte Carlo parallèles avec arbres reproductibles de sous-calculs; sa popularité découle probablement principalement de son nom accrocheur. Mais vous pouvez l'utiliser pour lancer des dés jouets comme cet exemple!
Une autre approche consiste à utiliser un simple générateur de nombres pseudo-aléatoires cryptographiques avec un petit état, comme un simple effacement de clé rapide PRNG , ou simplement un chiffrement de flux tel que AES-CTR ou ChaCha20 si vous êtes sûr ( par exemple , dans une simulation de Monte Carlo pour recherche en sciences naturelles) qu'il n'y a pas de conséquences négatives à prédire les résultats passés si l'état est un jour compromis.
std::uniform_int_distribution
pour les dés