J'ai donc vu un exposé intitulé Rand () considéré comme préjudiciable et il a préconisé l'utilisation du paradigme de distribution par moteur de la génération de nombres aléatoires par rapport au simple paradigme std::Rand()
plus modulus.
Cependant, je voulais voir les échecs de std::Rand()
de première main et j'ai donc fait une expérience rapide:
getRandNum_Old()
et getRandNum_New()
qui ont généré un nombre aléatoire compris entre 0 et 5 inclus à l’aide de std::Rand()
et std::mt19937
+ std::uniform_int_distribution
.Voici les résultats:
[OLD WAY]
Spread
mean: 346.554406
std dev: 110.318361
Time Taken (ms)
mean: 6.662910
std dev: 0.366301
[NEW WAY]
Spread
mean: 350.346792
std dev: 110.449190
Time Taken (ms)
mean: 28.053907
std dev: 0.654964
Étonnamment, la répartition globale des rouleaux était la même pour les deux méthodes. En d'autres termes, std::mt19937
+ std::uniform_int_distribution
n'était pas "plus uniforme" que la simple std::Rand()
+ %
. Une autre observation que j'ai faite est que la nouvelle était environ 4 fois plus lente que l'ancienne. Globalement, il me semblait que je payais un coût énorme en vitesse sans presque aucun gain en qualité.
Mon expérience est-elle imparfaite? Ou est-ce que std::Rand()
n'est pas si mal, et peut-être même mieux?
Pour référence, voici le code que j'ai utilisé dans son intégralité:
#include <cstdio>
#include <random>
#include <algorithm>
#include <chrono>
int getRandNum_Old() {
static bool init = false;
if (!init) {
std::srand(time(nullptr)); // Seed std::Rand
init = true;
}
return std::Rand() % 6;
}
int getRandNum_New() {
static bool init = false;
static std::random_device rd;
static std::mt19937 eng;
static std::uniform_int_distribution<int> dist(0,5);
if (!init) {
eng.seed(rd()); // Seed random engine
init = true;
}
return dist(eng);
}
template <typename T>
double mean(T* data, int n) {
double m = 0;
std::for_each(data, data+n, [&](T x){ m += x; });
m /= n;
return m;
}
template <typename T>
double stdDev(T* data, int n) {
double m = mean(data, n);
double sd = 0.0;
std::for_each(data, data+n, [&](T x){ sd += ((x-m) * (x-m)); });
sd /= n;
sd = sqrt(sd);
return sd;
}
int main() {
const int N = 960000; // Number of trials
const int M = 1000; // Number of simulations
const int D = 6; // Num sides on die
/* Do the things the "old" way (blech) */
int freqList_Old[D];
double stdDevList_Old[M];
double timeTakenList_Old[M];
for (int j = 0; j < M; j++) {
auto start = std::chrono::high_resolution_clock::now();
std::fill_n(freqList_Old, D, 0);
for (int i = 0; i < N; i++) {
int roll = getRandNum_Old();
freqList_Old[roll] += 1;
}
stdDevList_Old[j] = stdDev(freqList_Old, D);
auto end = std::chrono::high_resolution_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end-start);
double timeTaken = dur.count() / 1000.0;
timeTakenList_Old[j] = timeTaken;
}
/* Do the things the cool new way! */
int freqList_New[D];
double stdDevList_New[M];
double timeTakenList_New[M];
for (int j = 0; j < M; j++) {
auto start = std::chrono::high_resolution_clock::now();
std::fill_n(freqList_New, D, 0);
for (int i = 0; i < N; i++) {
int roll = getRandNum_New();
freqList_New[roll] += 1;
}
stdDevList_New[j] = stdDev(freqList_New, D);
auto end = std::chrono::high_resolution_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end-start);
double timeTaken = dur.count() / 1000.0;
timeTakenList_New[j] = timeTaken;
}
/* Display Results */
printf("[OLD WAY]\n");
printf("Spread\n");
printf(" mean: %.6f\n", mean(stdDevList_Old, M));
printf(" std dev: %.6f\n", stdDev(stdDevList_Old, M));
printf("Time Taken (ms)\n");
printf(" mean: %.6f\n", mean(timeTakenList_Old, M));
printf(" std dev: %.6f\n", stdDev(timeTakenList_Old, M));
printf("\n");
printf("[NEW WAY]\n");
printf("Spread\n");
printf(" mean: %.6f\n", mean(stdDevList_New, M));
printf(" std dev: %.6f\n", stdDev(stdDevList_New, M));
printf("Time Taken (ms)\n");
printf(" mean: %.6f\n", mean(timeTakenList_New, M));
printf(" std dev: %.6f\n", stdDev(timeTakenList_New, M));
}
Pratiquement toutes les implémentations de "vieux" Rand()
utilisent un LCG ; bien qu'ils ne soient généralement pas les meilleurs générateurs du moment, vous ne les verrez généralement pas échouer lors d'un test aussi élémentaire - l'écart moyen et standard est généralement obtenu, même par les pires PRNG.
Les échecs courants de "mauvais" - mais assez commun - les implémentations de Rand()
sont:
Rand_MAX
;Néanmoins, aucun d’entre eux n’est spécifique à l’API de Rand()
. Une implémentation particulière pourrait placer un générateur de famille xorshift derrière srand
/Rand
et obtenir algoritmiquement un état de la technique PRNG sans changement d’interface, donc pas de test similaire celui que vous avez montré montrerait une faiblesse dans la sortie.
Edit: @ R. note correctement que la Rand
/srand
L’interface est limitée par le fait que srand
prend un unsigned int
, ainsi tout générateur qu’une implémentation peut placer derrière est intrinsèquement limité à UINT_MAX
semences de départ possibles (et donc les séquences générées). C’est vrai, bien que l’API puisse être étendue de manière triviale pour que srand
prenne un unsigned long long
ou ajoute une surcharge srand(unsigned char *, size_t)
.
En effet, le problème réel avec Rand()
ne concerne pas beaucoup la mise en œuvre en principe mais:
Rand_MAX
de seulement 32767. Cependant, cela ne peut pas être changé facilement, cela romprait la compatibilité avec le passé - les personnes utilisant srand
avec une valeur de départ fixe pour des simulations reproductibles ne serait pas trop heureux (en effet, la mise en œuvre susmentionnée par IIRC remonte aux premières versions de Microsoft C - ou même de Lattice C - datant du milieu des années 80);interface simpliste; Rand()
fournit un seul générateur avec l'état global pour l'ensemble du programme. Bien que cela convienne parfaitement (et en fait très pratique) pour de nombreux cas d'utilisation simples, cela pose des problèmes:
Enfin, la Rand
état des lieux:
time(NULL)
n'est pas, car ce n'est pas assez granulaire, et souvent - pensez aux périphériques intégrés sans RTC - même pas aléatoires. suffisant).D'où le nouvel en-tête <random>
, qui tente de réparer ce désordre en fournissant des algorithmes qui sont:
... et un random_device
par défaut aussi pour les semer.
Maintenant, si vous me demandez, j'aurais aimé aussi une API simple construite au-dessus de cela pour les cas "facile", "devine un nombre" (similaire à la façon dont Python fournit l’API "compliquée", mais également le trivial random.randint
& Co., qui utilise un PRNGpré-ensemencé global pour nous, les gens simples qui aimeraient ne pas se noyer. appareils aléatoires/moteurs/adaptateurs/peu importe chaque fois que nous voulons extraire un numéro pour les cartes de bingo), mais il est vrai que vous pouvez facilement le construire par vous-même sur les installations actuelles (tout en construisant la "complète" API par-dessus une simple pas possible).
Enfin, pour revenir à votre comparaison de performances: comme d’autres l’ont déjà précisé, vous comparez une GCL rapide à une qualité plus lente (mais généralement considérée comme de meilleure qualité) Mersenne Twister; si vous êtes d'accord avec la qualité d'un LCG, vous pouvez utiliser std::minstd_Rand
au lieu de std::mt19937
.
En effet, après avoir peaufiné votre fonction pour utiliser std::minstd_Rand
et éviter les variables statiques inutiles pour l'initialisation
int getRandNum_New() {
static std::minstd_Rand eng{std::random_device{}()};
static std::uniform_int_distribution<int> dist{0, 5};
return dist(eng);
}
Je reçois 9 ms (ancien) contre 21 ms (nouveau); enfin, si je supprime dist
(qui, comparé à l'opérateur classique modulo, gère l'inclinaison de la distribution pour une plage de sortie non multiple de la plage d'entrée) et retourne à ce que vous faites dans getRandNum_Old()
int getRandNum_New() {
static std::minstd_Rand eng{std::random_device{}()};
return eng() % 6;
}
Je le réduis à 6 ms (donc, 30% plus rapide), probablement parce que, contrairement à l'appel à Rand()
, std::minstd_Rand
est plus facile à intégrer.
A propos, j’ai fait le même test en utilisant un roulé à la main (mais me conformant plus ou moins à l’interface standard de la bibliothèque) XorShift64*
, et il est 2,3 fois plus rapide que Rand()
(3,68 ms contre 8,61 ms); Etant donné que, contrairement au Mersenne Twister et aux divers LCG fournis, il réussit avec brio les suites de tests aléatoires actuelles et , il est incroyablement rapide. Je me demande pourquoi il n’est pas encore inclus dans la bibliothèque standard.
Si vous répétez votre expérience avec une plage supérieure à 5, vous obtiendrez probablement des résultats différents. Lorsque votre plage est nettement inférieure à Rand_MAX
, la plupart des applications ne posent pas de problème.
Par exemple, si nous avons un Rand_MAX
de 25, alors Rand() % 5
produira des nombres avec les fréquences suivantes:
0: 6
1: 5
2: 5
3: 5
4: 5
Comme il est garanti que Rand_MAX
sera supérieur à 32767 et que la différence de fréquences entre les moins probables et les plus probables n'est que de 1, pour les petits nombres, la distribution est suffisamment aléatoire pour la plupart des cas d'utilisation.
Tout d’abord, étonnamment, la réponse change en fonction de l’utilisation du nombre aléatoire. Si c'est pour conduire, disons, un changeur de couleur d'arrière-plan aléatoire, utiliser Rand () convient parfaitement. Si vous utilisez un nombre aléatoire pour créer une main de poker aléatoire ou une clé cryptographiquement sécurisée, alors tout va bien.
Prévisibilité: la séquence 012345012345012345012345 ... fournirait une distribution égale de chaque nombre de votre échantillon, mais ne serait évidemment pas aléatoire. Pour qu'une séquence soit aléatoire, la valeur de n + 1 ne peut pas être facilement prédite par la valeur de n (ou même par les valeurs de n, n-1, n-2, n-3, etc.). Clairement une séquence répétée des mêmes chiffres est un cas dégénéré, mais une séquence générée avec n’importe quel générateur de congruence linéaire peut être analysée; Si vous utilisez les paramètres par défaut prêts à l'emploi d'un LCG commun à partir d'une bibliothèque commune, une personne mal intentionnée pourrait "interrompre la séquence" sans trop d'effort. Dans le passé, plusieurs casinos en ligne (et certains castrés) ont été détruits par des machines utilisant de médiocres générateurs de nombres aléatoires. Même les personnes qui devraient savoir mieux ont été rattrapées; Il a été démontré que les puces TPM de plusieurs fabricants étaient plus faciles à décomposer que la longueur en bits des clés ne le prévoirait autrement en raison des mauvais choix effectués avec les paramètres de génération de clés.
Distribution: comme indiqué dans la vidéo, prendre un modulo de 100 (ou toute valeur non divisible de manière égale dans la longueur de la séquence) garantira que certains résultats seront au moins légèrement plus probables que d'autres. Dans l'univers de 32767 valeurs de départ possibles modulo 100, les nombres de 0 à 66 apparaîtront 328/327 (0,3%) plus souvent que les valeurs de 67 à 99; un facteur qui peut fournir un avantage à un attaquant.
La réponse correcte est: cela dépend de ce que vous entendez par "meilleur".
Les "nouveaux" moteurs <random>
ont été introduits en C++ il y a plus de 13 ans. Ils ne sont donc pas vraiment nouveaux. La bibliothèque C Rand()
a été introduite il y a plusieurs décennies et a été très utile à cette époque pour de nombreuses choses.
La bibliothèque standard C++ fournit trois classes de moteurs générateurs de nombres aléatoires: le linéaire congruentiel (dont Rand()
est un exemple), le retardé de Fibonacci et le Mersenne Twister. Il y a des compromis entre chaque classe et chaque classe est "meilleure" à certains égards. Par exemple, les LCG ont un très petit état et, si les bons paramètres sont choisis, assez rapidement sur les processeurs de bureau modernes. Les LFG ont un état plus étendu et utilisent uniquement des extractions de mémoire et des opérations d’addition. Ils sont donc très rapides sur les systèmes embarqués et les microcontrôleurs dépourvus de matériel mathématique spécialisé. Le MTG a un état énorme et est lent, mais peut avoir une très grande séquence non répétée avec d'excellentes caractéristiques spectrales.
Si aucun des générateurs fournis n'est suffisant pour votre utilisation spécifique, la bibliothèque standard C++ fournit également une interface pour un générateur de matériel ou votre propre moteur personnalisé. Aucun des générateurs n'est destiné à être utilisé de manière autonome: leur utilisation prévue est via un objet de distribution qui fournit une séquence aléatoire avec une fonction de distribution de probabilité particulière.
Un autre avantage de <random>
par rapport à Rand()
est que Rand()
utilise l'état global, qu'il n'est ni réentrant ni threadsafe, et qu'il permet une seule instance par processus. Si vous avez besoin d'un contrôle précis ou d'une prévisibilité (c'est-à-dire capable de reproduire un bogue en fonction de l'état initial du RNG), alors Rand()
est inutile. Les générateurs <random>
sont instanciés localement et ont un état sérialisable (et restaurable).