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

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.


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

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.


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.