<random> génère le même nombre sous Linux, mais pas sous Windows

90

Le code ci-dessous est destiné à générer une liste de cinq nombres pseudo-aléatoires dans l'intervalle [1 100]. Je lance le default_random_engineavec time(0), qui renvoie l'heure système en temps Unix . Lorsque je compile et exécute ce programme sur Windows 7 à l'aide de Microsoft Visual Studio 2013, il fonctionne comme prévu (voir ci-dessous). Quand je le fais dans Arch Linux avec le compilateur g ++, cependant, il se comporte étrangement.

Sous Linux, 5 numéros seront générés à chaque fois. Les 4 derniers numéros seront différents à chaque exécution (comme ce sera souvent le cas), mais le premier numéro restera le même.

Exemple de sortie de 5 exécutions sous Windows et Linux:

      | Windows:       | Linux:        
---------------------------------------
Run 1 | 54,01,91,73,68 | 25,38,40,42,21
Run 2 | 46,24,16,93,82 | 25,78,66,80,81
Run 3 | 86,36,33,63,05 | 25,17,93,17,40
Run 4 | 75,79,66,23,84 | 25,70,95,01,54
Run 5 | 64,36,32,44,85 | 25,09,22,38,13

Ajoutant au mystère, ce premier nombre s'incrémente périodiquement de un sous Linux. Après avoir obtenu les sorties ci-dessus, j'ai attendu environ 30 minutes et j'ai essayé à nouveau de trouver que le 1er numéro avait changé et qu'il était maintenant toujours généré en 26. Il a continué à augmenter de 1 périodiquement et est maintenant à 32. Il semble correspondre avec la valeur changeante de time(0).

Pourquoi le premier nombre change-t-il rarement d'une exécution à l'autre et, quand c'est le cas, augmente-t-il de 1?

Le code. Il imprime proprement les 5 chiffres et l'heure du système:

#include <iostream>
#include <random>
#include <time.h>

using namespace std;

int main()
{
    const int upper_bound = 100;
    const int lower_bound = 1;

    time_t system_time = time(0);    

    default_random_engine e(system_time);
    uniform_int_distribution<int> u(lower_bound, upper_bound);

    cout << '#' << '\t' << "system time" << endl
         << "-------------------" << endl;

    for (int counter = 1; counter <= 5; counter++)
    {
        int secret = u(e);
        cout << secret << '\t' << system_time << endl;
    }   

    system("pause");
    return 0;
}
Amin Mesbah
la source
3
Qu'est-ce que sizeof(time_t)vs. sizeof(default_random_engine::result_type)?
Mark Ransom
3
Notez que default_random_enginec'est complètement différent sur ces deux plates-formes.
TC
1
Cela peut toujours être aléatoire BTW.
Alec Teal
5
Est-ce que chaque programmeur passe par une phase où il pense que le temps est une bonne graine de générateur de nombres aléatoires?
OldFart
6
@OldFart Oui, ça s'appelle le monde universitaire.
Casey

Réponses:

141

Voici ce qui se passe:

  • default_random_enginedans libstdc ++ (la bibliothèque standard de GCC) est minstd_rand0, qui est un simple moteur congruentiel linéaire:

    typedef linear_congruential_engine<uint_fast32_t, 16807, 0, 2147483647> minstd_rand0;
  • La façon dont ce moteur génère des nombres aléatoires est x i + 1 = (16807x i + 0) mod 2147483647.

  • Par conséquent, si les graines sont différentes de 1, la plupart du temps, le premier nombre généré différera de 16807.

  • La portée de ce générateur est [1, 2147483646]. La façon dont libstdc ++ le uniform_int_distributionmappe à un entier dans la plage [1, 100] est essentiellement la suivante: générer un nombre n. Si le nombre n'est pas supérieur à 2147483600, retournez (n - 1) / 21474836 + 1; sinon, réessayez avec un nouveau numéro.

    Il devrait être facile de voir que dans la grande majorité des cas, deux ns qui ne diffèrent que de 16807 donneront le même nombre en [1, 100] selon cette procédure. En fait, on s'attendrait à ce que le nombre généré augmente de un environ toutes les 21474836/16807 = 1278 secondes ou 21,3 minutes, ce qui correspond assez bien à vos observations.

MSVC default_random_engineest mt19937, qui n'a pas ce problème.

TC
la source
36
Je me demande ce qui a poussé les développeurs de la bibliothèque standard de GCC à choisir un défaut aussi horrible.
CodesInChaos
13
@CodesInChaos Je ne sais pas si c'est lié à non, mais la chaîne d'outils MacOS / iOS utilise également le même moteur aléatoire horrible, ce qui fait que rand()% 7 retourne toujours 0
phuclv
7
@ LưuVĩnhPhúc Ne pas réparer rand()est quelque peu compréhensible (c'est de la merde héritée sans espoir). Utiliser un PRNG de niveau merde pour quelque chose de nouveau est inexcusable. Je considérerais même cela comme une violation de la norme, car la norme exige «de fournir au moins un comportement de moteur acceptable pour une utilisation relativement occasionnelle, inexpérimentée et / ou légère». que cette implémentation ne fournit pas car elle échoue de manière catastrophique, même pour des cas d'utilisation triviaux comme votre rand % 7exemple.
CodesInChaos
2
@CodesInChaos Pourquoi la correction n'est-elle pas rand()assez compréhensible exactement? Est-ce uniquement parce que personne n'aurait pensé à le faire?
user253751
2
@immibis L'API est tellement cassée que vous êtes mieux avec un remplaçant indépendant qui résout tous les problèmes. 1) Le remplacement de l'algorithme serait un changement radical, vous auriez donc probablement besoin d'un commutateur de compatibilité pour les programmes plus anciens. 2) La graine de srandest trop petite pour générer facilement des graines uniques. 3) Il renvoie un entier avec une limite supérieure définie par l'implémentation que l'appelant doit en quelque sorte réduire à un nombre dans la plage souhaitée, ce qui, lorsqu'il est fait correctement, est plus de travail que d'écrire un remplacement avec une API saine pour rand()4) Il utilise un état mutable global
CodesInChaos
30

L' std::default_random_engineimplémentation est définie. Utilisez std::mt19937ou à la std::mt19937_64place.

De plus std::timeet les ctimefonctions ne sont pas très précises, utilisez <chrono>plutôt les types définis dans l'en- tête:

#include <iostream>
#include <random>
#include <chrono>

int main()
{
    const int upper_bound = 100;
    const int lower_bound = 1;

    auto t = std::chrono::high_resolution_clock::now().time_since_epoch().count();

    std::mt19937 e;
    e.seed(static_cast<unsigned int>(t)); //Seed engine with timed value.
    std::uniform_int_distribution<int> u(lower_bound, upper_bound);

    std::cout << '#' << '\t' << "system time" << std::endl
    << "-------------------" << std::endl;

    for (int counter = 1; counter <= 5; counter++)
    {
        int secret = u(e);

        std::cout << secret << '\t' << t << std::endl;
    }   

    system("pause");
    return 0;
}
Casey
la source
3
Est-il souhaitable d'utiliser une heure plus précise lors de l'ensemencement d'un générateur de variables pseudo-aléatoires? C'est peut-être naïf, mais il semble qu'une inexactitude pourrait presque être souhaitable si elle introduit de l'entropie. (Sauf si vous voulez dire que c'est moins précis et qu'il en résulte donc moins de graines potentielles.)
Nat
15
Je suggérerais simplement d'utiliser std::random_deviceau lieu de current_time pour semer votre générateur aléatoire. Veuillez vérifier tout exemple de référence sur Random.
Aleksander Fular
5
Si vous ne voulez pas que quiconque devine votre graine (et donc reproduise votre séquence), moins de précision n'est pas la même chose que plus d'aléatoire. Allons à l'extrême: arrondissez votre graine au jour suivant (ou à l'année?) -> deviner est facile. Utilisez la précision femtoseconde -> Beaucoup de devinettes à faire ...
linac
2
@ChemicalEngineer La granularité de ctimeest de 1 seconde. La granularité des std::chronoimplémentations est définie par l'utilisateur, par défaut, pour std::high_resolution_clock(dans Visual Studio, c'est un typedef pour std::steady_clock), nanosecondes, mais peut choisir une mesure beaucoup plus petite, donc beaucoup plus précise.
Casey
2
@linac Si vous vouliez des propriétés cryptographiques, vous utiliseriez le prng approprié (et non celui utilisé dans cette réponse). Et bien sûr, les semences basées sur le temps sont également hors de question, quelle que soit la précision promise.
Cthulhu
-2

Sous Linux, la fonction aléatoire n'est pas une fonction aléatoire au sens probabiliste du terme, mais un générateur de nombres pseudo aléatoires. Il est salé avec une graine, et sur la base de cette graine, les nombres qui sont produits sont pseudo aléatoires et uniformément distribués. La méthode Linux a l'avantage que dans la conception de certaines expériences utilisant des informations provenant de populations, la répétition de l'expérience avec des ajustements connus des informations d'entrée peut être mesurée. Lorsque le programme final est prêt pour les tests en conditions réelles, le sel (graine) peut être créé en demandant à l'utilisateur de déplacer la souris, de mélanger le mouvement de la souris avec quelques frappes et d'ajouter un tiret de microsecondes depuis le début de la dernière mise sous tension.

La graine de nombre aléatoire de Windows est obtenue à partir de la collection de numéros de souris, de clavier, de réseau et d'heure du jour. Ce n'est pas reproductible. Mais cette valeur de sel peut être réinitialisée à une graine connue, si comme mentionné ci-dessus, on est impliqué dans la conception d'une expérience.

Oh oui, Linux a deux générateurs de nombres aléatoires. Un, la valeur par défaut est modulo 32bits, et l'autre est modulo 64bits. Votre choix dépend des besoins de précision et de la quantité de temps de calcul que vous souhaitez utiliser pour vos tests ou votre utilisation réelle.

Leslie Satenstein
la source
5
Je ne sais pas pourquoi vous parlez d'algorithme de génération de semences. OP utilise clairement le temps système comme une graine. Aussi, pouvez-vous ajouter des références àcollection of mouse, keyboard, network and time of day numbers
paramètres régionaux par défaut