<random> genera lo stesso numero in Linux, ma non in Windows


90

Il codice seguente ha lo scopo di generare un elenco di cinque numeri pseudo-casuali nell'intervallo [1,100]. Semina il default_random_enginecon time(0), che restituisce l'ora di sistema in unix tempo . Quando compilo ed eseguo questo programma su Windows 7 utilizzando Microsoft Visual Studio 2013, funziona come previsto (vedi sotto). Quando lo faccio in Arch Linux con il compilatore g ++, tuttavia, si comporta in modo strano.

In Linux, verranno generati 5 numeri ogni volta. Gli ultimi 4 numeri saranno diversi ad ogni esecuzione (come spesso accade), ma il primo numero rimarrà lo stesso.

Output di esempio da 5 esecuzioni su Windows e 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

Aggiungendo al mistero, quel primo numero aumenta periodicamente di uno su Linux. Dopo aver ottenuto le uscite di cui sopra, ho aspettato circa 30 minuti e ho provato di nuovo a scoprire che il 1 ° numero era cambiato e ora veniva sempre generato come 26. Ha continuato ad aumentare periodicamente di 1 ed ora è a 32. Sembra corrispondere con il valore variabile di time(0).

Perché il primo numero cambia raramente tra le serie e poi, quando lo fa, aumenta di 1?

Il codice. Stampa ordinatamente i 5 numeri e l'ora di sistema:

#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;
}

3
Cos'è sizeof(time_t)vs. sizeof(default_random_engine::result_type)?
Mark Ransom

3
Nota che default_random_engineè completamente diverso su queste due piattaforme.
TC

1
Può ancora essere casuale BTW.
Alec Teal,

5
Ogni programmatore passa attraverso una fase in cui pensa che il tempo sia un buon seme di generatore di numeri casuali?
OldFart

6
@OldFart Sì, si chiama università.
Casey

Risposte:


141

Ecco cosa sta succedendo:

  • default_random_enginein libstdc ++ (la libreria standard di GCC) è minstd_rand0, che è un semplice motore lineare congruente:

    typedef linear_congruential_engine<uint_fast32_t, 16807, 0, 2147483647> minstd_rand0;
  • Il modo in cui questo motore genera numeri casuali è x i + 1 = (16807x i + 0) mod 2147483647.

  • Pertanto, se i semi sono diversi di 1, il più delle volte il primo numero generato differirà di 16807.

  • La portata di questo generatore è [1, 2147483646]. Il modo in cui libstdc ++ uniform_int_distributionlo associa a un numero intero compreso nell'intervallo [1, 100] è essenzialmente questo: genera un numero n. Se il numero non è maggiore di 2147483600, restituisci (n - 1) / 21474836 + 1; in caso contrario, riprova con un nuovo numero.

    Dovrebbe essere facile vedere che nella stragrande maggioranza dei casi, due ns che differiscono solo di 16807 produrranno lo stesso numero in [1, 100] con questa procedura. In effetti, ci si aspetterebbe che il numero generato aumenti di uno circa ogni 21474836/16807 = 1278 secondi o 21,3 minuti, il che concorda abbastanza bene con le tue osservazioni.

MSVC di default_random_engineè mt19937, che non ha questo problema.


36
Mi chiedo cosa abbia spinto gli sviluppatori della libreria standard di GCC a scegliere un valore predefinito così orribile.
CodesInChaos

13
@CodesInChaos Non so se è correlato a no, ma anche la toolchain di MacOS / iOS utilizza lo stesso orribile motore casuale, facendo in modo che rand()% 7 restituisca sempre 0
phuclv

7
@ LưuVĩnhPhúc Non riparare rand()è in qualche modo comprensibile (è una schifezza senza speranza). Usare un PRNG di merda per qualcosa di nuovo è imperdonabile. Considererei anche questa una violazione dello standard, poiché lo standard richiede "fornire un comportamento del motore almeno accettabile per un uso relativamente casuale, inesperto e / o leggero". che questa implementazione non fornisce poiché fallisce in modo catastrofico anche per casi d'uso banali come il tuo rand % 7esempio.
CodesInChaos

2
@CodesInChaos Perché la correzione non è rand()esattamente comprensibile? È solo perché nessuno potrebbe aver pensato di farlo?
user253751

2
@immibis L'API è così rotta che stai meglio con una sostituzione indipendente che risolve tutti i problemi. 1) La sostituzione dell'algoritmo sarebbe un cambiamento radicale, quindi probabilmente avresti bisogno di un interruttore di compatibilità per i programmi più vecchi. 2) Il seme di srandè troppo piccolo per generare facilmente semi unici. 3) Restituisce un intero con un limite superiore definito dall'implementazione che il chiamante deve in qualche modo ridurre a un numero nell'intervallo desiderato, che se fatto correttamente è più lavoro che scrivere una sostituzione con un'API sana per rand()4) Utilizza lo stato mutabile globale
CodesInChaos

30

L' std::default_random_engineimplementazione è definita. Usa std::mt19937o std::mt19937_64invece.

Inoltre std::timee le ctimefunzioni non sono molto precise, usa invece i tipi definiti <chrono>nell'intestazione:

#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;
}

3
È desiderabile utilizzare un tempo più preciso durante il seeding di un generatore di variabili pseudo-casuali? Forse questo è ingenuo, ma sembra che l'imprecisione potrebbe quasi essere desiderabile se introduce l'entropia. (A meno che tu non voglia dire che è meno preciso e quindi si traduce in un numero sostanzialmente inferiore di potenziali semi.)
Nat

15
Suggerirei solo di utilizzare std::random_deviceinvece di current_time per il seeding del generatore casuale. Si prega di controllare qualsiasi esempio cppreference su Random.
Aleksander Fular

5
Se non vuoi che nessuno indovini il tuo seme (e quindi riproduca la tua sequenza) meno precisione non è la stessa cosa di più casualità. Andiamo all'estremo: arrotondare il seme al giorno successivo (o anno?) -> indovinare è facile. Usa la precisione al femtosecondo -> Un sacco di supposizioni da fare ...
linac

2
@ChemicalEngineer La granularità di ctimeè 1 secondo. La granularità delle std::chronoimplementazioni è definita dall'utente, per impostazione predefinita, per std::high_resolution_clock(in Visual Studio è un typedef per std::steady_clock), nanosecondi ma può scegliere una misurazione molto più piccola, quindi molto più precisa.
Casey

2
@linac Se volessi proprietà crittografiche, useresti prng appropriato (non uno usato in questa risposta). E ovviamente anche il seme basato sul tempo è fuori discussione, indipendentemente dalla precisione promessa.
Cthulhu

-2

In Linux, la funzione casuale non è una funzione casuale nel senso probabilistico del modo, ma un generatore di numeri pseudo casuali. Viene salato con un seme e, in base a quel seme, i numeri che vengono prodotti sono pseudo casuali e distribuiti uniformemente. Il metodo Linux ha il vantaggio che nella progettazione di alcuni esperimenti che utilizzano informazioni provenienti da popolazioni, è possibile misurare la ripetizione dell'esperimento con modifiche note delle informazioni di input. Quando il programma finale è pronto per i test nella vita reale, il sale (seme) può essere creato chiedendo all'utente di muovere il mouse, mescolare il movimento del mouse con alcune sequenze di tasti e aggiungere un trattino di conteggi di microsecondi dall'inizio del l'ultima accensione.

Il seed dei numeri casuali di Windows è ottenuto dalla raccolta di numeri di mouse, tastiera, rete e ora del giorno. Non è ripetibile. Ma questo valore di sale può essere reimpostato su un seme noto, se, come menzionato sopra, si è coinvolti nella progettazione di un esperimento.

Oh sì, Linux ha due generatori di numeri casuali. Uno, il valore predefinito è modulo 32 bit e l'altro modulo 64 bit. La scelta dipende dalle esigenze di precisione e dalla quantità di tempo di elaborazione che si desidera consumare per i test o per l'utilizzo effettivo.


5
Non sono sicuro del motivo per cui stai parlando dell'algoritmo di generazione dei semi. L'OP usa chiaramente l'ora di sistema come seme. Inoltre, puoi aggiungere alcuni riferimenti acollection of mouse, keyboard, network and time of day numbers
impostazione locale predefinita
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.