<aleatorio> genera el mismo número en Linux, pero no en Windows

90

El siguiente código está destinado a generar una lista de cinco números pseudoaleatorios en el intervalo [1100]. Siembro el default_random_enginecon time(0), que devuelve la hora del sistema en tiempo Unix . Cuando compilo y ejecuto este programa en Windows 7 usando Microsoft Visual Studio 2013, funciona como se esperaba (ver más abajo). Cuando lo hago en Arch Linux con el compilador g ++, sin embargo, se comporta de manera extraña.

En Linux, se generarán 5 números cada vez. Los últimos 4 números serán diferentes en cada ejecución (como suele ser el caso), pero el primer número seguirá siendo el mismo.

Salida de ejemplo de 5 ejecuciones en Windows y 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 el misterio, ese primer número se incrementa periódicamente en uno en Linux. Después de obtener los resultados anteriores, esperé unos 30 minutos e intenté nuevamente encontrar que el primer número había cambiado y ahora siempre se generaba como 26. Ha continuado aumentando en 1 periódicamente y ahora está en 32. Parece corresponder con el valor cambiante de time(0).

¿Por qué el primer número rara vez cambia entre ejecuciones y luego, cuando lo hace, se incrementa en 1?

El código. Imprime cuidadosamente los 5 números y la hora del 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
fuente
3
¿Qué es sizeof(time_t)vs. sizeof(default_random_engine::result_type)?
Mark Ransom
3
Tenga en cuenta que default_random_enginees completamente diferente en esas dos plataformas.
TC
1
Todavía puede ser aleatorio por cierto.
Alec Teal
5
¿Todos los programadores pasan por una fase en la que piensan que el tiempo es una buena semilla generadora de números aleatorios?
OldFart
6
@OldFart Sí, se llama academia.
Casey

Respuestas:

141

Esto es lo que está pasando:

  • default_random_engineen libstdc ++ (la biblioteca estándar de GCC) es minstd_rand0, que es un motor congruencial lineal simple:

    typedef linear_congruential_engine<uint_fast32_t, 16807, 0, 2147483647> minstd_rand0;
  • La forma en que este motor genera números aleatorios es x i + 1 = (16807x i + 0) mod 2147483647.

  • Por lo tanto, si las semillas son diferentes en 1, la mayoría de las veces el primer número generado diferirá en 16807.

  • El rango de este generador es [1, 2147483646]. La forma en que libstdc ++ lo uniform_int_distributionasigna a un número entero en el rango [1, 100] es esencialmente esta: generar un número n. Si el número no es mayor que 2147483600, regrese (n - 1) / 21474836 + 1; de lo contrario, vuelva a intentarlo con un número nuevo.

    Debería ser fácil ver que en la gran mayoría de los casos, dos ns que difieren solo en 16807 producirán el mismo número en [1, 100] bajo este procedimiento. De hecho, uno esperaría que el número generado aumentara en uno aproximadamente cada 21474836/16807 = 1278 segundos o 21,3 minutos, lo que concuerda bastante bien con sus observaciones.

MSVC default_random_enginees mt19937, que no tiene este problema.

TC
fuente
36
Me pregunto qué tuvieron los desarrolladores de la biblioteca estándar de GCC para elegir un valor predeterminado tan horrible.
CodesInChaos
13
@CodesInChaos No sé si está relacionado con no, pero la cadena de herramientas de MacOS / iOS también usa el mismo motor aleatorio horrible, lo que hace que rand()% 7 siempre devuelva 0
phuclv
7
@ LưuVĩnhPhúc No arreglar rand()es algo comprensible (es una mierda heredada sin esperanza). Usar un PRNG de mierda para algo nuevo es imperdonable. Incluso consideraría esto una violación estándar, ya que la norma requiere "proporcionar al menos un comportamiento aceptable del motor para un uso relativamente casual, inexperto y / o ligero". que esta implementación no proporciona ya que falla catastróficamente incluso para casos de uso triviales como su rand % 7ejemplo.
CodesInChaos
2
@CodesInChaos ¿Por qué la corrección no es rand()algo comprensible exactamente? ¿Es solo porque a nadie se le habría ocurrido hacerlo?
user253751
2
@immibis La API está tan rota que es mejor que tenga un reemplazo independiente que solucione todos los problemas. 1) Reemplazar el algoritmo sería un cambio importante, por lo que probablemente necesitaría un interruptor de compatibilidad para programas más antiguos. 2) La semilla de srandes demasiado pequeña para generar fácilmente semillas únicas. 3) Devuelve un número entero con un límite superior definido por la implementación que la persona que llama tiene que reducir de alguna manera a un número en el rango deseado, que cuando se hace correctamente es más trabajo que escribir un reemplazo con una API sana para rand()4) Utiliza un estado mutable global
CodesInChaos
30

La std::default_random_engineimplementación está definida. Utilice std::mt19937o en su std::mt19937_64lugar.

Además, std::timey las ctimefunciones no son muy precisas, utilice los tipos definidos en el <chrono>encabezado en su lugar:

#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
fuente
3
¿Es deseable utilizar una hora más precisa al sembrar un generador de variables pseudoaleatorias? Quizás esto sea ingenuo, pero parece que la inexactitud podría ser casi deseable si introduce entropía. (A menos que decir que es menos preciso y por lo tanto se traduce en un menor número de semillas materialmente posibles.)
Nat
15
Solo sugeriría usar en std::random_devicelugar de current_time para sembrar su generador aleatorio. Consulte cualquier ejemplo de referencia de cpp sobre Random.
Aleksander Fular
5
Si no quiere que nadie adivine su semilla (y por lo tanto reproduzca su secuencia), menos precisión no es lo mismo que más aleatoriedad. Vayamos al extremo: redondee su semilla al día siguiente (¿o al año?) -> adivinar es fácil. Use precisión de femtosegundos -> Muchas conjeturas para hacer ...
linac
2
@ChemicalEngineer La granularidad de ctimees 1 segundo. La granularidad de las std::chronoimplementaciones está definida por el usuario, por defecto, por std::high_resolution_clock(en Visual Studio es un typedef para std::steady_clock), nanosegundos, pero puede elegir una medida mucho más pequeña, por lo tanto, mucho más precisa.
Casey
2
@linac Si quisiera propiedades criptográficas, usaría prng apropiado (no uno usado en esta respuesta). Y, por supuesto, la semilla basada en el tiempo también está fuera de discusión, sin importar la precisión prometida.
Cthulhu
-2

En Linux, la función aleatoria no es una función aleatoria en el sentido probabilístico del camino, sino un generador de números pseudoaleatorios. Se sala con una semilla, y en base a esa semilla, los números que se producen son pseudoaleatorios y uniformemente distribuidos. El método Linux tiene la ventaja de que en el diseño de ciertos experimentos utilizando información de poblaciones, se puede medir la repetición del experimento con ajustes conocidos de la información de entrada. Cuando el programa final está listo para la prueba de la vida real, la sal (semilla) se puede crear pidiendo al usuario que mueva el mouse, mezcle el movimiento del mouse con algunas pulsaciones de teclas y agregue una pizca de conteos de microsegundos desde el comienzo de el último encendido.

La semilla de números aleatorios de Windows se obtiene de la colección de números de mouse, teclado, red y hora del día. No es repetible. Pero este valor de sal puede restablecerse a una semilla conocida, si, como se mencionó anteriormente, uno está involucrado en el diseño de un experimento.

Oh, sí, Linux tiene dos generadores de números aleatorios. Uno, el valor predeterminado es módulo de 32 bits y el otro es módulo de 64 bits. Su elección depende de las necesidades de precisión y de la cantidad de tiempo de cálculo que desee consumir para sus pruebas o uso real.

Leslie Satenstein
fuente
5
No estoy seguro de por qué está hablando del algoritmo de generación de semillas. OP claramente usa el tiempo del sistema como semilla. Además, se puede añadir algunas referencias acollection of mouse, keyboard, network and time of day numbers
por defecto local