<random> gera o mesmo número no Linux, mas não no Windows

90

O código a seguir tem como objetivo gerar uma lista de cinco números pseudo-aleatórios no intervalo [1.100]. Eu semeio o default_random_enginecom time(0), que retorna a hora do sistema em tempo unix . Quando eu compilo e executo este programa no Windows 7 usando o Microsoft Visual Studio 2013, ele funciona como esperado (veja abaixo). Quando faço isso no Arch Linux com o compilador g ++, no entanto, ele se comporta de maneira estranha.

No Linux, 5 números serão gerados a cada vez. Os últimos 4 números serão diferentes em cada execução (como geralmente será o caso), mas o primeiro número permanecerá o mesmo.

Exemplo de resultado de 5 execuções no 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

Para aumentar o mistério, esse primeiro número incrementa periodicamente em um no Linux. Depois de obter os resultados acima, esperei cerca de 30 minutos e tentei novamente para descobrir que o primeiro número havia mudado e agora estava sempre sendo gerado como 26. Ele continuou a aumentar em 1 periodicamente e agora está em 32. Parece corresponder com a alteração do valor de time(0).

Por que o primeiro número raramente muda entre as execuções e, quando muda, aumenta em 1?

O código. Ele imprime perfeitamente os 5 números e a hora do 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;
}
Amin Mesbah
fonte
3
O que é sizeof(time_t)vs. sizeof(default_random_engine::result_type)?
Mark Ransom
3
Observe que default_random_engineé completamente diferente nessas duas plataformas.
TC
1
Ainda pode ser aleatório BTW.
Alec Teal
5
Todo programador passa por uma fase em que pensa que o tempo é uma boa semente do gerador de números aleatórios?
OldFart
6
@OldFart Sim, é chamado de academia.
Casey

Respostas:

141

Aqui está o que está acontecendo:

  • default_random_engineem libstdc ++ (biblioteca padrão do GCC) é minstd_rand0, que é um mecanismo congruencial linear simples:

    typedef linear_congruential_engine<uint_fast32_t, 16807, 0, 2147483647> minstd_rand0;
  • A forma como este mecanismo gera números aleatórios é x i + 1 = (16807x i + 0) mod 2147483647.

  • Portanto, se as sementes forem diferentes em 1, na maioria das vezes o primeiro número gerado será diferente em 16807.

  • O intervalo deste gerador é [1, 2147483646]. A forma como o libstdc ++ uniform_int_distributionmapeia para um inteiro no intervalo [1, 100] é essencialmente esta: gere um número n. Se o número não for maior que 2147483600, retorne (n - 1) / 21474836 + 1; caso contrário, tente novamente com um novo número.

    Deve ser fácil ver que na grande maioria dos casos, dois ns que diferem por apenas 16807 produzirão o mesmo número em [1,100] sob este procedimento. Na verdade, seria de se esperar que o número gerado aumentasse em um a cada 21474836/16807 = 1278 segundos ou 21,3 minutos, o que concorda muito bem com suas observações.

O MSVC default_random_engineé mt19937, que não tem esse problema.

TC
fonte
36
Eu me pergunto o que deu aos desenvolvedores da biblioteca padrão do GCC para escolher um padrão tão horrível.
CodesInChaos
13
@CodesInChaos Não sei se está relacionado ou não, mas o conjunto de ferramentas MacOS / iOS também usa o mesmo mecanismo aleatório horrível, fazendo rand()% 7 sempre retornar 0
phuclv
7
@ LưuVĩnhPhúc Não consertar rand()é algo compreensível (é uma porcaria de legado sem esperança). Usar um PRNG de nível de merda para algo novo é imperdoável. Eu até consideraria isso uma violação do padrão, já que o padrão exige "fornecer pelo menos um comportamento de motor aceitável para uso relativamente casual, inexperiente e / ou leve". que esta implementação não fornece, uma vez que falha catastroficamente, mesmo para casos de uso triviais como o seu rand % 7exemplo.
CodesInChaos
2
@CodesInChaos Por que a correção não é rand()compreensível exatamente? É apenas porque ninguém poderia ter pensado em fazer isso?
user253751
2
@immibis A API está tão danificada que é melhor você ter uma substituição independente que corrige todos os problemas. 1) Substituir o algoritmo seria uma alteração importante, então você provavelmente precisaria de uma chave de compatibilidade para programas mais antigos. 2) A semente de srandé muito pequena para gerar facilmente sementes únicas. 3) Ele retorna um número inteiro com um limite superior definido pela implementação que o chamador tem de reduzir de alguma forma para um número na faixa desejada, o que quando feito corretamente é mais trabalhoso do que escrever uma substituição com uma API sã para rand()4) Ele usa o estado mutável global
CodesInChaos
30

A std::default_random_engineimplementação é definida. Use std::mt19937ou em seu std::mt19937_64lugar.

Além disso, std::timee as ctimefunções não são muito precisas, use os tipos definidos no <chrono>cabeçalho:

#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
fonte
3
É desejável usar um tempo mais preciso ao semear um gerador de variável pseudo-aleatória? Talvez isso seja ingênuo, mas parece que a imprecisão pode ser quase desejável se introduzir entropia. (A menos que você queira dizer que é menos preciso e, portanto, resulta em menos sementes potenciais.)
Nat,
15
Eu apenas sugeriria usar em std::random_devicevez de current_time para semear seu gerador aleatório. Por favor, verifique qualquer exemplo de cppreference sobre Random.
Aleksander Fular
5
Se você não quer que ninguém adivinhe sua semente (e portanto reproduza sua sequência), menos precisão não é o mesmo que mais aleatoriedade. Vamos ao extremo: arredondar sua semente para o dia seguinte (ou ano?) -> adivinhar é fácil. Use a precisão de femtossegundo -> Muitas adivinhações para fazer ...
linac
2
@ChemicalEngineer A granularidade de ctimeé 1 segundo. A granularidade das std::chronoimplementações é definida pelo usuário, padronizando para, para std::high_resolution_clock(no Visual Studio é um typedef para std::steady_clock) nanossegundos, mas pode escolher uma medida muito menor, portanto, muito mais precisa.
Casey
2
@linac Se você quisesse propriedades criptográficas, você usaria prng apropriado (não aquele usado nesta resposta). E é claro que a semente baseada no tempo também está fora de questão, não importa a precisão prometida.
Cthulhu
-2

No Linux, a função aleatória não é uma função aleatória no sentido probabilístico da maneira, mas um gerador de números pseudo-aleatórios. É salgado com uma semente e, com base nessa semente, os números produzidos são pseudo-aleatórios e uniformemente distribuídos. A maneira Linux tem a vantagem de que, no projeto de certos experimentos usando informações de populações, a repetição do experimento com ajustes conhecidos de informações de entrada pode ser medida. Quando o programa final está pronto para o teste da vida real, o sal (semente) pode ser criado pedindo ao usuário para mover o mouse, misturar o movimento do mouse com algumas teclas e adicionar um traço de contagem de microssegundos desde o início de a última energia ligada.

A semente de números aleatórios do Windows é obtida a partir da coleção de números de mouse, teclado, rede e hora do dia. Não é repetível. Mas esse valor de sal pode ser redefinido para uma semente conhecida, se, como mencionado acima, alguém estiver envolvido no planejamento de um experimento.

Sim, o Linux tem dois geradores de números aleatórios. Um, o padrão é o módulo 32bits e o outro é o módulo 64bits. Sua escolha depende das necessidades de precisão e da quantidade de tempo de computação que você deseja consumir para o seu teste ou uso real.

Leslie Satenstein
fonte
5
Não sei por que você está falando sobre algoritmo de geração de sementes. O OP claramente usa a hora do sistema como uma semente. Além disso, você pode adicionar algumas referências acollection of mouse, keyboard, network and time of day numbers
localidade padrão