Cet exemple de code illustre que std::rand
est un cas de balivernes de culte du cargo hérité qui devrait vous faire lever les sourcils à chaque fois que vous le verrez.
Il y a plusieurs problèmes ici :
Le contrat que les gens supposent généralement - même les pauvres âmes infortunées qui ne savent pas mieux et ne pensent pas précisément en ces termes - est que rand
des échantillons de la distribution uniforme sur les entiers en 0, 1, 2, , RAND_MAX
et chaque appel donne lieu à un indépendant échantillon.
Le premier problème est que le contrat supposé, à savoir des échantillons aléatoires uniformes indépendants à chaque appel, n'est pas réellement ce que dit la documentation - et dans la pratique, les implémentations n'ont historiquement pas réussi à fournir même le plus petit simulacre d'indépendance. Par exemple, dans le document C99 §7.20.2.1, "L'autorité de surveillance de l'environnement". rand
fonction" dit, sans élaborer :
El rand
calcule une séquence d'entiers pseudo-aléatoires dans l'intervalle 0 à RAND_MAX
.
Cette phrase n'a aucun sens, car le caractère pseudo-aléatoire est une propriété d'une fonction (ou famille de fonctions ), et non d'un entier, mais cela n'empêche pas les bureaucrates de l'ISO d'abuser de ce langage. Après tout, les seuls lecteurs que cela pourrait déranger savent qu'il vaut mieux ne pas lire la documentation de la norme rand
de peur que les cellules de leur cerveau ne se décomposent.
Une implémentation historique typique en C fonctionne comme suit :
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;
}
Ceci 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 nombres entiers pairs et impairs dans des appels consécutifs.
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 indépendant échantillons aléatoires sur n'importe quelle distribution supportée sur les entiers pairs et impairs. C'est ainsi qu'est apparu un culte du fret selon lequel il fallait se débarrasser des bits d'ordre inférieur pour chasser la bête insaisissable du "meilleur hasard". (Alerte spoiler : ce n'est pas un terme technique. C'est un signe que la personne dont vous lisez la prose ne sait pas de quoi elle parle, ou pense que le terme "meilleur hasard" n'a pas de sens. vous sont ignorants et doivent être traités avec condescendance).
Le deuxième problème est que même si chaque appel a été échantillonné indépendamment à partir 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 jet de dé, sauf si RAND_MAX
est congruente à -1 modulo 6. Contre-exemple simple : Si RAND_MAX
= 6, alors de rand()
toutes les issues ont la même probabilité 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 est l'échantillonnage par rejet : à plusieurs reprises tirer un échantillon aléatoire uniforme indépendant s
de 0, 1, 2, , RAND_MAX
et rejeter (par exemple) les résultats 0, 1, 2, , ((RAND_MAX + 1) % 6) - 1
Si vous obtenez l'un de ces résultats, recommencez ; sinon, rendez-le. 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 de rand()
que nous acceptons est divisible de manière égale par 6, et chaque résultat possible de s % 6
est obtenu par le même nombre de accepté les résultats de rand()
donc si rand()
est uniformément distribué, alors il en est de même pour s
. Il n'y a pas de lié sur le nombre d'essais, mais le nombre attendu est inférieure à 2, et la probabilité de réussite croît de façon exponentielle avec le nombre d'essais.
Le choix de dont les résultats de rand()
que vous rejetez n'a pas d'importance, à condition que vous en fassiez correspondre un nombre égal à chaque nombre entier inférieur à 6. Le code sur cppreference.com fait un différents en raison du premier problème ci-dessus, à savoir que rien n'est garanti quant à la distribution ou à l'indépendance des résultats de l'opération. rand()
Dans la pratique, les bits de poids faible présentent des motifs qui ne semblent pas suffisamment aléatoires (sans compter que la sortie suivante est une fonction déterministe de la précédente).
Exercice pour le lecteur : Prouvez que le code de cppreference.com produit une distribution uniforme sur les jets de dé si rand()
donne une distribution uniforme sur 0, 1, 2, , RAND_MAX
.
Exercice pour le lecteur : Pourquoi préférer l'un ou l'autre des sous-ensembles à rejeter ? Quel calcul est nécessaire pour chaque essai dans les deux cas ?
Un troisième problème est que l'espace des graines est si petit que même si la graine est distribuée uniformément, un adversaire connaissant votre programme et un résultat mais pas la graine peut facilement prédire la graine et les résultats suivants, ce qui les fait paraître moins aléatoires après tout. Donc ne pensez même pas à l'utiliser pour la cryptographie.
Vous pouvez opter pour la voie de la sur-ingénierie fantaisiste et l'approche C++11. std::uniform_int_distribution
avec un dispositif aléatoire approprié et votre moteur aléatoire préféré, comme le toujours populaire twister de Mersenne. std::mt19937
pour jouer aux dés avec votre cousin de quatre ans, mais même cela n'est pas adapté à la génération de clés cryptographiques - et le tordeur de Mersenne est également très gourmand en espace, avec un état de plusieurs kilo-octets qui fait des ravages dans le cache de votre processeur et un temps de configuration obscène, donc il n'est pas bon même pour.., par exemple Ce logiciel permet de réaliser des simulations de Monte Carlo parallèles avec des arbres de sous-calculs reproductibles ; sa popularité est probablement due à son nom accrocheur. Mais vous pouvez l'utiliser pour lancer des dés comme cet exemple !
Une autre approche consiste à utiliser un générateur de nombres pseudo-aléatoires cryptographique simple avec un petit état, tel qu'un simple effacement rapide des clés PRNG ou simplement un chiffrement par flux tel que AES-CTR ou ChaCha20 si vous êtes sûr de vous ( par exemple dans une simulation de Monte Carlo pour la recherche en sciences naturelles) qu'il n'y a pas de conséquences négatives à prévoir les résultats passés si l'état est jamais compromis.