Tag Archives: c++11

rand() moi un entier !

Cet article est une synthèse de plusieurs conférences, de la documentation C++ et de mon expérience sur la génération de nombre aléatoire en C++.

Mon besoin

Je suis le dĂ©veloppeur de Rolisteam. Un logiciel pour faire du jeu de rĂ´le en ligne. Il intègre une solution badass (oui, je m’envoie des fleurs) de lancer de dĂ©s. J’ai passĂ© beaucoup de temps entre juillet 2014 et fĂ©vrier 2015 pour produire un langage de script interprĂ©tĂ© de lancement de commandes de dĂ©s. Vous pouvez y jeter un Ĺ“il sur ce lien: DiceParser. Bref, j’ai crĂ©e tout un système pour lancer les dĂ©s et faire des opĂ©rations sur les rĂ©sultats: tri, relance, filtre, explosion, arithmĂ©tique, gestion des prioritĂ©s mathĂ©matiques et il est mĂŞme capable de gĂ©nĂ©rer l’arbre d’exĂ©cution avec dot.

L’aléatoire

Pour la partie alĂ©atoire dans tout le système, j’ai rĂ©utilisĂ© le code historique de rolistik (l’ancĂŞtre de rolisteam).
Certains utilisateurs sont venus me voir pour me demander de vérifier le système d’aléatoire car ils avaient fait des tests et avez déterminé que Rolisteam favorisait les résultats haut; ou bas selon les personnes.
J’avais droit Ă  un tableau de statistique rĂ©alisĂ© sur quelques dizaines de lancer de dĂ©s.
Argument qu’il est facile de dĂ©truire avec une Ă©tude statistique plus riche en lancer. DiceParser fonctionne en ligne de commande, donc très facile Ă  «scripter». Bref, des gens avaient des doutes sans rĂ©elle preuve.
Du coup, je me suis un peu renseignĂ© s’il n’y avait pas des mĂ©thodes plus «C++» avec le C++11, C++14 et/ou C++17.

L’existant

Quand il s’agit de gĂ©nĂ©rer des nombres alĂ©atoires, la première implĂ©mentation qu’on apprend est souvent celle ci :

[pastacode lang=”cpp” manual=”int%20resultat%20%3D%20std::rand()%25MAX%2Bdebut%3B” message=”usage classique de rand();” highlight=”” provider=”manual”/]

Je pense que tous les Ă©tudiants ont implĂ©mentĂ© cette mĂ©thode, dans leurs premières annĂ©es d’Ă©tudes.  Elle est facile Ă  comprendre. Elle est disponible dans beaucoup de langages. Elle vient du C. Elle met en avant le modulo, ce qui lui donne peut-ĂŞtre une valeur pĂ©dagogique. Elle suffit dans de très nombreux cas.

Nous allons voir un peu ce qui cloche avec cette méthode.

Les défauts

1/ Pauvre entropie
DĂ©jĂ , rand() ou std::rand() sont dĂ©finis dans le “man” comme une solution pauvre pour obtenir de l’alĂ©atoire. Il est prĂ©cisĂ© que les vieilles implĂ©mentations de rand() ou les implĂ©mentations sur d’autres systèmes fournissent un alĂ©atoire pauvre sur les bits de poids faibles. Cela explique peut-ĂŞtre pourquoi certains utilisateurs sous Windows se plaignent.
Donc l’entropie de rand n’est pas bonne. Il existe une autre mĂ©thode: random() qui fonctionne normalement mieux, avec un pĂ©riode beaucoup plus grande.

2/ Erreur de répartition
Ces deux mĂ©thodes gĂ©nèrent un nombre entre 0 et RAND_MAX. Maintenant, nous souhaitons obtenir des rĂ©sultats sur un dĂ©s Ă  100 faces. (C’est un format de dĂ©s courant en Jeux de rĂ´le).
Imaginons que RAND_MAX vaut 32767.

Si je découpe la valeur maximum en tranche de 100 cela donne ceci:

[0 : 99] => 100 valeurs
[100 : 199] => 100 valeurs
[200 : 299] => 100 valeurs
:
:
[32700 : 32767 ] => 67 valeurs.

Nous avons 327 tranches complètes et une dernière de 67.  L’usage du modulo vient faire la correspondance entre la valeur tirĂ©e dans l’espace de valeurs dĂ©sirĂ©es. Cela favorise les rĂ©sultats entre 0 et 67. Il y a une chance supplĂ©mentaire de tomber en dessous de 67. La diffĂ©rence est probablement nĂ©gligeables mais tout de mĂŞme, cela me pose problème d’avoir cela dans mon logiciel.

3/ Problème de portabilité

La valeur RAND_MAX vaut au minimum 32767 dans la norme. Elle diffèrent en fonction des implĂ©mentations, nous avons vu Ă©galement que la portabilitĂ© de la mĂ©thode rand() n’est pas garantie.
Cette somme de problèmes vient conforter les utilisateurs de Rolisteam dans leur idée de problème relatif au système de dés. Deux problèmes de portabilité se cumulent.

Le constat de ces erreurs m’a poussĂ© Ă  chercher une solution moderne pour gĂ©nĂ©rer ces nombres.

Dans les API C++ est arrivĂ© un nouveau “module” avec le C++11. Il s’agit de random.

#include <random>

Ce module présente toute une série de classes pour générer des nombres aléatoires et les répartir.
Il propose un nombre intĂ©ressant d’algorithmes ou de moteurs diffĂ©rents pour la gĂ©nĂ©ration. Je ne suis pas un expert en gĂ©nĂ©ration pseudo-alĂ©atoire de nombre. Du coup, le nom des algorithmes ne me parle pas.
Cependant, il semble que deux éléments sortent du lot: le std::random_device et le std::mt19937.

Le std::random_device

Il est dĂ©fini dans la documentation comme: Un gĂ©nĂ©rateur de nombre alĂ©atoire non dĂ©terministe. En gros, cela signifie qu’on ne peut pas prĂ©voir son comportement.
D’habitude, vous initialisez votre mĂ©thode alĂ©atoire par un seed. Il peut ĂŞtre intĂ©ressant pour des tests automatiques ou une rĂ©solution de bug de pouvoir rejouer une sĂ©quence entière de nombre alĂ©atoire.
Pour atteindre cet objectif, il suffit de mettre la mĂŞme seed. Donc vous avez lĂ  une mĂ©thode de nombre alĂ©atoire qui est dĂ©terministe. random_device n’a pas de seed. Il n’a pas besoin d’ĂŞtre initialiser. Il est donc impossible de lui faire “jouer” deux fois la mĂŞme chose. Dans mon cas, cela me satisfait complètement.

C’est facile Ă  mettre en place:

[pastacode lang=”cpp” manual=”std%3A%3Arandom_device%20rd%3B%0Astd%3A%3Auniform_int_distribution%3Cint%3E%20dist(1%2C%2010)%3B%0Aint%20randomNumber%20%3D%20dist(rd)%3B%0Astd%3A%3Acout%20%3C%3C%20%20randomNumber%20%3C%3C%20std%3A%3Aendl%3B” message=”” highlight=”” provider=”manual”/]

On gĂ©nère une nombre et on le distribue entre 1 et 10. Cela suffit. Il y a rien besoin de plus. C’est propre, Ă©lĂ©gant etc.
J’ai implĂ©mentĂ© cette mĂ©thode dans rolisteam 1.8, malgrĂ© les avertissements qu’on trouve Ă  droit Ă  gauche. Cela donne de bons rĂ©sultats sous Linux. J’ai fait une version pour Windows et lĂ , c’est le drame.
L’entropie du module sous windows est de zĂ©ro. Cela signifie qu’il renvoie toujours le mĂŞme rĂ©sultat. Bref, les dĂ©s Ă©taient toujours de la mĂŞme valeur.
La solution du random_device ne marche pas. Elle n’est pas portable.

Le std::mt19937

Il reste donc la mĂ©thode du std::mt19937. Elle fait toujours partie de l’API de <random>. Voici l’implĂ©mentation finale dans rolisteam.

[pastacode lang=”cpp” manual=”%2F%2F%20dans%20le%20constructeur%20de%20ma%20classe%20d%C3%A9%0Aauto%20seed%20%3D%20std%3A%3Achrono%3A%3Ahigh_resolution_clock%3A%3Anow().time_since_epoch().count()%3B%0Am_rng%20%3D%20std%3A%3Amt19937(quintptr(this)%2Bseed)%3B%0A%0A%2F%2Fdans%20la%20fonction%20lancer%20(roll())%0Astd%3A%3Auniform_int_distribution%3Cqint64%3E%20dist(m_base%2Cm_faces)%3B%0Aqint64%20value%20%3D%20dist(m_rng)%3B” message=”L’implementation dans rolisteam.” highlight=”” provider=”manual”/]

On peut voir qu’elle est plus proche de l’ancienne mĂ©thode car Il est nĂ©cessaire de l’initialiser avec un seed. Pour cela, il est Ă©galement possible d’utiliser une API C++11: <chrono>.

[pastacode lang=”cpp” manual=”%20%23include%20%3Cchrono%3E%0A%E2%80%A6%0Aauto%20seed%20%3D%20std%3A%3Achrono%3A%3Ahigh_resolution_clock%3A%3Anow().time_since_epoch().count()%3B%0Am_rng%20%3D%20std%3A%3Amt19937(quintptr(this)%2Bseed)%3B” message=”Politique Agricole Commune” highlight=”” provider=”manual”/]

 

Autres méthodes:

Conclusion:

J’espère avoir fait le tour des nouvelles API pour la gĂ©nĂ©ration alĂ©atoire de nombres entiers en C++. Il est important de garder Ă  l’esprit que l’API <random> fournit bien sur bien plus de chose que ce que j’ai montrĂ©. Je n’ai pas fait d’étude statistique pour dĂ©montrer que c’est mieux maintenant. Dans tous les cas, j’ai implĂ©mentĂ© le device_random dans mon lecteur audio maison, et je dĂ©couvre plein de nouvelle musique de ma collection.

Les sources:

CppCon 2016: Cheinan Marks “I Just Wanted a Random Integer!”:
https://www.youtube.com/watch?v=4_QO1nm7uJs
Stephan T. Lavavej – rand() Considered Harmful:
https://channel9.msdn.com/Events/GoingNative/2013/rand-Considered-Harmful