¿Es este un algoritmo aleatorio "suficientemente bueno"; ¿Por qué no se usa si es más rápido?

171

Hice una clase llamada QuickRandom, y su trabajo es producir números aleatorios rápidamente. Es realmente simple: solo toma el valor anterior, multiplica por a doubley toma la parte decimal.

Aquí está mi QuickRandomclase en su totalidad:

public class QuickRandom {
    private double prevNum;
    private double magicNumber;

    public QuickRandom(double seed1, double seed2) {
        if (seed1 >= 1 || seed1 < 0) throw new IllegalArgumentException("Seed 1 must be >= 0 and < 1, not " + seed1);
        prevNum = seed1;
        if (seed2 <= 1 || seed2 > 10) throw new IllegalArgumentException("Seed 2 must be > 1 and <= 10, not " + seed2);
        magicNumber = seed2;
    }

    public QuickRandom() {
        this(Math.random(), Math.random() * 10);
    }

    public double random() {
        return prevNum = (prevNum*magicNumber)%1;
    }

}

Y aquí está el código que escribí para probarlo:

public static void main(String[] args) {
        QuickRandom qr = new QuickRandom();

        /*for (int i = 0; i < 20; i ++) {
            System.out.println(qr.random());
        }*/

        //Warm up
        for (int i = 0; i < 10000000; i ++) {
            Math.random();
            qr.random();
            System.nanoTime();
        }

        long oldTime;

        oldTime = System.nanoTime();
        for (int i = 0; i < 100000000; i ++) {
            Math.random();
        }
        System.out.println(System.nanoTime() - oldTime);

        oldTime = System.nanoTime();
        for (int i = 0; i < 100000000; i ++) {
            qr.random();
        }
        System.out.println(System.nanoTime() - oldTime);
}

Es un algoritmo muy simple que simplemente multiplica el doble anterior por un "número mágico" doble. Lo uní bastante rápido, por lo que probablemente podría mejorarlo, pero extrañamente, parece estar funcionando bien.

Esta es una salida de muestra de las líneas comentadas en el mainmétodo:

0.612201846732229
0.5823974655091941
0.31062451498865684
0.8324473610354004
0.5907187526770246
0.38650264675748947
0.5243464344127049
0.7812828761272188
0.12417247811074805
0.1322738256858378
0.20614642573072284
0.8797579436677381
0.022122999476108518
0.2017298328387873
0.8394849894162446
0.6548917685640614
0.971667953190428
0.8602096647696964
0.8438709031160894
0.694884972852229

Hm. Bastante al azar. De hecho, eso funcionaría para un generador de números aleatorios en un juego.

Aquí está la salida de muestra de la parte no comentada:

5456313909
1427223941

¡Guauu! Se realiza casi 4 veces más rápido que Math.random.

Recuerdo haber leído en algún lugar que Math.randomusaba System.nanoTime()toneladas de módulo loco y cosas de división. ¿Es eso realmente necesario? Mi algoritmo funciona mucho más rápido y parece bastante aleatorio.

Tengo dos preguntas:

  • ¿Es mi algoritmo "suficientemente bueno" (para, por ejemplo, un juego, donde los números realmente aleatorios no son demasiado importantes)?
  • ¿Por qué hace Math.randomtanto cuando parece simple multiplicación y cortar el decimal será suficiente?
tckmn
fuente
154
"parece bastante aleatorio"; debería generar un histograma y ejecutar una autocorrelación en su secuencia ...
Oliver Charlesworth
63
Significa que "parece bastante aleatorio" no es realmente una medida objetiva de aleatoriedad y debe obtener algunas estadísticas reales.
Matt H
23
@Doorknob: en términos simples, debe investigar si sus números tienen una distribución "plana" entre 0 y 1, y ver si hay patrones periódicos / repetitivos a lo largo del tiempo.
Oliver Charlesworth
22
Prueba new QuickRandom(0,5)o new QuickRandom(.5, 2). Ambos emitirán repetidamente 0 para su número.
FrankieTheKneeMan
119
Escribir su propio algoritmo de generación de números aleatorios es como escribir su propio algoritmo de cifrado. Hay tanta técnica anterior, por personas hipercalificadas, que no tiene sentido gastar su tiempo tratando de hacerlo bien. No hay ninguna razón para no usar las funciones de la biblioteca Java, y si realmente desea escribir la suya por alguna razón, visite Wikipedia y busque algoritmos como Mersenne Twister.
steveha

Respuestas:

351

Su QuickRandomimplementación no tiene realmente una distribución uniforme. Las frecuencias son generalmente más altas en los valores más bajos, mientras que Math.random()tiene una distribución más uniforme. Aquí hay un SSCCE que muestra que:

package com.stackoverflow.q14491966;

import java.util.Arrays;

public class Test {

    public static void main(String[] args) throws Exception {
        QuickRandom qr = new QuickRandom();
        int[] frequencies = new int[10];
        for (int i = 0; i < 100000; i++) {
            frequencies[(int) (qr.random() * 10)]++;
        }
        printDistribution("QR", frequencies);

        frequencies = new int[10];
        for (int i = 0; i < 100000; i++) {
            frequencies[(int) (Math.random() * 10)]++;
        }
        printDistribution("MR", frequencies);
    }

    public static void printDistribution(String name, int[] frequencies) {
        System.out.printf("%n%s distribution |8000     |9000     |10000    |11000    |12000%n", name);
        for (int i = 0; i < 10; i++) {
            char[] bar = "                                                  ".toCharArray(); // 50 chars.
            Arrays.fill(bar, 0, Math.max(0, Math.min(50, frequencies[i] / 100 - 80)), '#');
            System.out.printf("0.%dxxx: %6d  :%s%n", i, frequencies[i], new String(bar));
        }
    }

}

El resultado promedio se ve así:

QR distribution |8000     |9000     |10000    |11000    |12000
0.0xxx:  11376  :#################################                 
0.1xxx:  11178  :###############################                   
0.2xxx:  11312  :#################################                 
0.3xxx:  10809  :############################                      
0.4xxx:  10242  :######################                            
0.5xxx:   8860  :########                                          
0.6xxx:   9004  :##########                                        
0.7xxx:   8987  :#########                                         
0.8xxx:   9075  :##########                                        
0.9xxx:   9157  :###########                                       

MR distribution |8000     |9000     |10000    |11000    |12000
0.0xxx:  10097  :####################                              
0.1xxx:   9901  :###################                               
0.2xxx:  10018  :####################                              
0.3xxx:   9956  :###################                               
0.4xxx:   9974  :###################                               
0.5xxx:  10007  :####################                              
0.6xxx:  10136  :#####################                             
0.7xxx:   9937  :###################                               
0.8xxx:  10029  :####################                              
0.9xxx:   9945  :###################    

Si repite la prueba, verá que la distribución QR varía mucho, dependiendo de las semillas iniciales, mientras que la distribución MR es estable. A veces alcanza la distribución uniforme deseada, pero más de las veces no lo hace. Este es uno de los ejemplos más extremos, está incluso más allá de los límites del gráfico:

QR distribution |8000     |9000     |10000    |11000    |12000
0.0xxx:  41788  :##################################################
0.1xxx:  17495  :##################################################
0.2xxx:  10285  :######################                            
0.3xxx:   7273  :                                                  
0.4xxx:   5643  :                                                  
0.5xxx:   4608  :                                                  
0.6xxx:   3907  :                                                  
0.7xxx:   3350  :                                                  
0.8xxx:   2999  :                                                  
0.9xxx:   2652  :                                                  
BalusC
fuente
17
+1 para datos numéricos, aunque mirar números sin procesar puede ser engañoso ya que no significa que tengan una diferencia estadísticamente significativa.
Maciej Piechotka
16
Estos resultados varían mucho con las semillas iniciales que se pasan QuickRandom. A veces, está cerca del uniforme, a veces es mucho peor que esto.
Petr Janeček
68
@ BlueRaja-DannyPflughoeft Cualquier PRNG donde la calidad de la salida depende en gran medida de los valores iniciales (a diferencia de las constantes internas) me parece roto.
un CVn
22
Primera regla de estadística: trazar los datos . Su análisis es perfecto, pero trazar un histograma lo muestra mucho más rápido. ;-) (Y son dos líneas en R.)
Konrad Rudolph
37
Citas obligatorias: "Cualquiera que considere métodos aritméticos para producir dígitos aleatorios está, por supuesto, en un estado de pecado". - John von Neumann (1951) "Cualquiera que no haya visto la cita anterior en al menos 100 lugares probablemente no sea muy viejo". - DV Pryor (1993) "Los generadores de números aleatorios no deben elegirse al azar". - Donald Knuth (1986)
Happy Green Kid Naps
133

Lo que está describiendo es un tipo de generador aleatorio llamado generador congruencial lineal . El generador funciona de la siguiente manera:

  • Comience con un valor semilla y un multiplicador.
  • Para generar un número aleatorio:
    • Multiplica la semilla por el multiplicador.
    • Establezca la semilla igual a este valor.
    • Devuelve este valor.

Este generador tiene muchas propiedades agradables, pero tiene problemas significativos como una buena fuente aleatoria. El artículo de Wikipedia vinculado anteriormente describe algunas de las fortalezas y debilidades. En resumen, si necesita buenos valores aleatorios, probablemente este no sea un enfoque muy bueno.

¡Espero que esto ayude!

templatetypedef
fuente
@ louism- No es realmente "aleatorio", per se. Los resultados serán deterministas. Dicho esto, no pensé en eso al escribir mi respuesta; ¿Quizás alguien pueda aclarar ese detalle?
templatetypedef
2
Los errores aritméticos de coma flotante están diseñados para la implementación. Hasta donde yo sé, son consistentes para una determinada plataforma, pero pueden diferir, por ejemplo, entre diferentes teléfonos móviles y entre arquitecturas de PC. Aunque a veces se agregan 'bits de protección' adicionales al hacer una serie de cálculos de coma flotante en una fila, y la presencia o ausencia de estos bits de protección puede hacer que un cálculo difiera sutilmente en el resultado. (los bits de protección son, por ejemplo, la expansión de un doble de 64 bits a 80 bits)
Patashu
2
Además, tenga en cuenta que la teoría detrás de los LCRNG asume que está trabajando con números enteros. Lanzarle números de punto flotante no producirá la misma calidad de resultados.
duskwuff -inactive-
1
@duskwuff, tienes razón. Pero si el hardware de coma flotante sigue reglas sensatas, hacer esto es lo mismo que hacerlo modulando el tamaño de la mantisa, y se aplica la teoría. Solo necesita cuidado extra en lo que está haciendo.
vonbrand
113

La función de su número aleatorio es deficiente, ya que tiene muy poco estado interno: el número que genera la función en cualquier paso depende completamente del número anterior. Por ejemplo, si suponemos que magicNumberes 2 (a modo de ejemplo), entonces la secuencia:

0.10 -> 0.20

está fuertemente reflejado por secuencias similares:

0.09 -> 0.18
0.11 -> 0.22

En muchos casos, esto generará correlaciones notables en su juego; por ejemplo, si realiza llamadas sucesivas a su función para generar coordenadas X e Y para los objetos, los objetos formarán patrones diagonales claros.

A menos que tenga una buena razón para creer que el generador de números aleatorios está ralentizando su aplicación (y esto es MUY improbable), no hay una buena razón para intentar escribir la suya.

duskwuff -inactive-
fuente
36
¿+1 para una respuesta práctica ... usar esto en un shoot em up y generar enemigos a lo largo de diagonales para múltiples disparos a la cabeza épicos? : D
wim
@wim: no necesitas un PRNG si quieres esos patrones.
Lie Ryan
109

El verdadero problema con esto es que su histograma de salida depende en gran medida de la semilla inicial: la mayor parte del tiempo terminará con una salida casi uniforme, pero muchas veces tendrá una salida claramente no uniforme.

Inspirado en este artículo sobre cuán mala es la rand()función de php , hice algunas imágenes de matriz aleatorias usando QuickRandomy System.Random. Esta ejecución muestra cómo a veces la semilla puede tener un efecto negativo (en este caso, favoreciendo números más bajos) donde System.Randomes bastante uniforme.

QuickRandom

System.Random

Peor aún

Si inicializamos a QuickRandommedida new QuickRandom(0.01, 1.03)que obtenemos esta imagen:

El código

using System;
using System.Drawing;
using System.Drawing.Imaging;

namespace QuickRandomTest
{
    public class QuickRandom
    {
        private double prevNum;
        private readonly double magicNumber;

        private static readonly Random rand = new Random();

        public QuickRandom(double seed1, double seed2)
        {
            if (seed1 >= 1 || seed1 < 0) throw new ArgumentException("Seed 1 must be >= 0 and < 1, not " + seed1);
            prevNum = seed1;
            if (seed2 <= 1 || seed2 > 10) throw new ArgumentException("Seed 2 must be > 1 and <= 10, not " + seed2);
            magicNumber = seed2;
        }

        public QuickRandom()
            : this(rand.NextDouble(), rand.NextDouble() * 10)
        {
        }

        public double Random()
        {
            return prevNum = (prevNum * magicNumber) % 1;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var rand = new Random();
            var qrand = new QuickRandom();
            int w = 600;
            int h = 600;
            CreateMatrix(w, h, rand.NextDouble).Save("System.Random.png", ImageFormat.Png);
            CreateMatrix(w, h, qrand.Random).Save("QuickRandom.png", ImageFormat.Png);
        }

        private static Image CreateMatrix(int width, int height, Func<double> f)
        {
            var bitmap = new Bitmap(width, height);
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    var c = (int) (f()*255);
                    bitmap.SetPixel(x, y, Color.FromArgb(c,c,c));
                }
            }

            return bitmap;
        }
    }
}
Callum Rogers
fuente
2
Buen código Si, eso es genial. También solía hacer eso a veces, es difícil obtener una medida cuantificable, pero es otra buena manera de ver la secuencia. Y si desea echar un vistazo a las secuencias de más de ancho * alto, puede ampliar la siguiente imagen con este píxel por píxel. Sin embargo, creo que la imagen QuickRandom es mucho más agradable estéticamente, debido a que está texturizada como una alfombra de algas.
Cris Stringfellow
La parte estéticamente agradable es cómo la secuencia tiende a aumentar a medida que avanza a lo largo de cada fila (y luego vuelve al inicio), ya que la magicNumbermultiplicación produce un número similar a prevNum, que muestra la falta de aleatoriedad. Si usamos las semillas new QuickRandom(0.01, 1.03), obtenemos esto i.imgur.com/Q1Yunbe.png !
Callum Rogers
Sí, gran análisis. Dado que simplemente multiplica el mod 1 por una constante claramente antes de que ocurra el ajuste, habrá el aumento que describas. Parece que esto podría evitarse si tomáramos los decimales menos significativos multiplicando por mil millones y luego reduciendo mod una paleta de 256 colores.
Cris Stringfellow
¿Me puede decir qué utilizó para generar esas imágenes de salida? Matlab?
uday
@uDaY: Eche un vistazo al código, C # y System.Drawing.Bitmap.
Callum Rogers
37

Un problema con su generador de números aleatorios es que no hay un 'estado oculto': si sé qué número aleatorio devolvió en la última llamada, sé cada número aleatorio que enviará hasta el final de los tiempos, ya que solo hay uno posible resultado siguiente, y así sucesivamente.

Otra cosa a considerar es el 'período' de su generador de números aleatorios. Obviamente, con un tamaño de estado finito, igual a la porción de mantisa de un doble, solo podrá devolver como máximo 2 ^ 52 valores antes del bucle. Pero ese es el mejor de los casos: ¿puede probar que no hay bucles de los períodos 1, 2, 3, 4 ...? Si los hay, su RNG tendrá un comportamiento horrible y degenerado en esos casos.

Además, ¿su generación de números aleatorios tendrá una distribución uniforme para todos los puntos de partida? Si no es así, su RNG estará sesgado, o peor, sesgado de diferentes maneras dependiendo de la semilla inicial.

Si puedes responder a todas estas preguntas, genial. Si no puede, entonces sabe por qué la mayoría de las personas no reinventan la rueda y usan un generador de números aleatorios comprobado;)

(Por cierto, un buen adagio es: el código más rápido es el código que no se ejecuta. Podría hacer el aleatorio () más rápido del mundo, pero no es bueno si no es muy aleatorio)

Patashu
fuente
8
Hay por lo menos un bucle trivial en este generador para todas las semillas 0 -> 0. Dependiendo de la semilla, puede haber muchos otros. (Por ejemplo, con una semilla de 3,0, 0.5 -> 0.5, 0.25 -> 0.75 -> 0.25, 0.2 -> 0.6 -> 0.8 -> 0.4 -> 0.2, etc.)
duskwuff -inactive-
36

Una prueba común que siempre hacía cuando desarrollaba PRNG era:

  1. Convertir salida a valores char
  2. Escribir valor de caracteres en un archivo
  3. Comprimir archivo

Esto me permitió repetir rápidamente ideas que eran PRNG "suficientemente buenas" para secuencias de alrededor de 1 a 20 megabytes. También proporcionó una mejor imagen de arriba hacia abajo que simplemente inspeccionarla a simple vista, ya que cualquier PRNG "suficientemente bueno" con media palabra de estado podría exceder rápidamente la capacidad de sus ojos para ver el punto del ciclo.

Si fuera realmente exigente, podría tomar los buenos algoritmos y ejecutar las pruebas DIEHARD / NIST en ellos, para obtener más información, y luego regresar y ajustar un poco más.

La ventaja de la prueba de compresión, a diferencia de un análisis de frecuencia, es que, trivialmente, es fácil construir una buena distribución: simplemente genera un bloque de 256 longitudes que contenga todos los caracteres de los valores 0 - 255, y haz esto 100,000 veces. Pero esta secuencia tiene un ciclo de longitud 256.

Una distribución sesgada, incluso por un pequeño margen, debe ser recogida por un algoritmo de compresión, particularmente si le da suficiente (digamos 1 megabyte) de la secuencia para trabajar. Si algunos caracteres, o bigrams, o n-gramas ocurren con mayor frecuencia, un algoritmo de compresión puede codificar este sesgo de distribución a códigos que favorecen las ocurrencias frecuentes con palabras de código más cortas, y usted obtiene un delta de compresión.

Dado que la mayoría de los algoritmos de compresión son rápidos y no requieren implementación (ya que los sistemas operativos los tienen por ahí), la prueba de compresión es muy útil para calificar rápidamente la aprobación / falla de un PRNG que podría estar desarrollando.

¡Buena suerte con tus experimentos!

Oh, realicé esta prueba en el rng que tienes arriba, usando el siguiente pequeño mod de tu código:

import java.io.*;

public class QuickRandom {
    private double prevNum;
    private double magicNumber;

    public QuickRandom(double seed1, double seed2) {
        if (seed1 >= 1 || seed1 < 0) throw new IllegalArgumentException("Seed 1 must be >= 0 and < 1, not " + seed1);
        prevNum = seed1;
        if (seed2 <= 1 || seed2 > 10) throw new IllegalArgumentException("Seed 2 must be > 1 and <= 10, not " + seed2);
        magicNumber = seed2;
    }

    public QuickRandom() {
        this(Math.random(), Math.random() * 10);
    }

    public double random() {
        return prevNum = (prevNum*magicNumber)%1;
    }

    public static void main(String[] args) throws Exception {
        QuickRandom qr = new QuickRandom();
        FileOutputStream fout = new FileOutputStream("qr20M.bin");

        for (int i = 0; i < 20000000; i ++) {
            fout.write((char)(qr.random()*256));
        }
    }
}

Los resultados fueron:

Cris-Mac-Book-2:rt cris$ zip -9 qr20M.zip qr20M.bin2
adding: qr20M.bin2 (deflated 16%)
Cris-Mac-Book-2:rt cris$ ls -al
total 104400
drwxr-xr-x   8 cris  staff       272 Jan 25 05:09 .
drwxr-xr-x+ 48 cris  staff      1632 Jan 25 05:04 ..
-rw-r--r--   1 cris  staff      1243 Jan 25 04:54 QuickRandom.class
-rw-r--r--   1 cris  staff       883 Jan 25 05:04 QuickRandom.java
-rw-r--r--   1 cris  staff  16717260 Jan 25 04:55 qr20M.bin.gz
-rw-r--r--   1 cris  staff  20000000 Jan 25 05:07 qr20M.bin2
-rw-r--r--   1 cris  staff  16717402 Jan 25 05:09 qr20M.zip

Consideraría un PRNG bueno si el archivo de salida no se pudiera comprimir en absoluto. Para ser honesto, no pensé que tu PRNG lo haría tan bien, solo el 16% en ~ 20 Megs es bastante impresionante para una construcción tan simple. Pero todavía lo considero un fracaso.

Cris Stringfellow
fuente
2
Imaginándolo o no, tengo la misma idea con el zip hace años cuando pruebo mis generadores aleatorios.
Aristos
1
Gracias @Alexandre C. y Aristos y aidan. Te creo.
Cris Stringfellow
33

El generador aleatorio más rápido que podría implementar es este:

ingrese la descripción de la imagen aquí

XD, bromea aparte, además de todo lo que se dice aquí, me gustaría contribuir citando que probar secuencias aleatorias "es una tarea difícil" [1], y hay varias pruebas que verifican ciertas propiedades de números pseudoaleatorios, puede encontrar un Muchos de ellos aquí: http://www.random.org/analysis/#2005

Una forma sencilla de evaluar la "calidad" del generador aleatorio es la antigua prueba Chi Square.

static double chisquare(int numberCount, int maxRandomNumber) {
    long[] f = new long[maxRandomNumber];
    for (long i = 0; i < numberCount; i++) {
        f[randomint(maxRandomNumber)]++;
    }

    long t = 0;
    for (int i = 0; i < maxRandomNumber; i++) {
        t += f[i] * f[i];
    }
    return (((double) maxRandomNumber * t) / numberCount) - (double) (numberCount);
}

Citando [1]

La idea de la prueba χ² es verificar si los números producidos se distribuyen o no de manera razonable. Si generamos N números positivos menores que r , entonces esperaríamos obtener aproximadamente N / r números de cada valor. Pero --- y esta es la esencia del asunto --- las frecuencias de aparición de todos los valores no deberían ser exactamente las mismas: ¡eso no sería aleatorio!

Simplemente calculamos la suma de los cuadrados de las frecuencias de ocurrencia de cada valor, escaladas por la frecuencia esperada, y luego restamos el tamaño de la secuencia. Este número, el "estadístico χ²", puede expresarse matemáticamente como

fórmula de chi cuadrado

Si la estadística χ² está cerca de r , entonces los números son aleatorios; si está muy lejos, entonces no lo están. Las nociones de "cerca" y "lejos" se pueden definir con mayor precisión: existen tablas que indican exactamente cómo se relacionan las estadísticas con las propiedades de las secuencias aleatorias. Para la prueba simple que estamos realizando, la estadística debe estar dentro de 2√r

Usando esta teoría y el siguiente código:

abstract class RandomFunction {
    public abstract int randomint(int range); 
}

public class test {
    static QuickRandom qr = new QuickRandom();

    static double chisquare(int numberCount, int maxRandomNumber, RandomFunction function) {
        long[] f = new long[maxRandomNumber];
        for (long i = 0; i < numberCount; i++) {
            f[function.randomint(maxRandomNumber)]++;
        }

        long t = 0;
        for (int i = 0; i < maxRandomNumber; i++) {
            t += f[i] * f[i];
        }
        return (((double) maxRandomNumber * t) / numberCount) - (double) (numberCount);
    }

    public static void main(String[] args) {
        final int ITERATION_COUNT = 1000;
        final int N = 5000000;
        final int R = 100000;

        double total = 0.0;
        RandomFunction qrRandomInt = new RandomFunction() {
            @Override
            public int randomint(int range) {
                return (int) (qr.random() * range);
            }
        }; 
        for (int i = 0; i < ITERATION_COUNT; i++) {
            total += chisquare(N, R, qrRandomInt);
        }
        System.out.printf("Ave Chi2 for QR: %f \n", total / ITERATION_COUNT);        

        total = 0.0;
        RandomFunction mathRandomInt = new RandomFunction() {
            @Override
            public int randomint(int range) {
                return (int) (Math.random() * range);
            }
        };         
        for (int i = 0; i < ITERATION_COUNT; i++) {
            total += chisquare(N, R, mathRandomInt);
        }
        System.out.printf("Ave Chi2 for Math.random: %f \n", total / ITERATION_COUNT);
    }
}

Obtuve el siguiente resultado:

Ave Chi2 for QR: 108965,078640
Ave Chi2 for Math.random: 99988,629040

Que, para QuickRandom, está lejos de r (fuera de r ± 2 * sqrt(r))

Dicho esto, QuickRandom podría ser rápido pero (como se indicó en otras respuestas) no es bueno como generador de números aleatorios


[1] SEDGEWICK ROBERT, Algoritmos en C , Addinson Wesley Publishing Company, 1990, páginas 516 a 518

higuaro
fuente
9
+1 para xkcd, que es un sitio web increíble (oh, y la gran respuesta): P
tckmn
1
Gracias, y sí bastidores xkcd! XD
higuaro
La teoría está bien, pero la ejecución es deficiente: el código es susceptible al desbordamiento de enteros. En Java, todos int[]se inicializan a cero, por lo que no es necesario para esta parte. Lanzar para flotar no tiene sentido cuando trabajas con dobles. Por último: llamar a los nombres de los métodos random1 y random2 es bastante divertido.
bestsss
@bestsss ¡Gracias por las observaciones! Hice una traducción directa del código C y no le presté mucha atención = (. Hice algunas modificaciones y actualicé la respuesta. Agradecería cualquier sugerencia adicional
higuaro
14

Reuní una maqueta rápida de su algoritmo en JavaScript para evaluar los resultados. Genera 100,000 enteros aleatorios de 0 a 99 y rastrea la instancia de cada entero.

Lo primero que noto es que es más probable que obtenga un número bajo que uno alto. Lo ves más cuando seed1es alto y seed2bajo. En un par de casos, obtuve solo 3 números.

En el mejor de los casos, su algoritmo necesita un poco de refinamiento.

gilly3
fuente
8

Si la Math.Random()función llama al sistema operativo para obtener la hora del día, entonces no puede compararla con su función. Su función es un PRNG, mientras que esa función se esfuerza por obtener números aleatorios reales. Manzanas y naranjas.

Su PRNG puede ser rápido, pero no tiene suficiente información de estado para lograr un largo período antes de que se repita (y su lógica no es lo suficientemente sofisticada como para lograr los períodos posibles con tanta información de estado).

El período es la duración de la secuencia antes de que su PRNG comience a repetirse. Esto sucede tan pronto como la máquina PRNG realiza una transición de estado a un estado que es idéntico a algún estado pasado. A partir de ahí, repetirá las transiciones que comenzaron en ese estado. Otro problema con los PRNG puede ser un bajo número de secuencias únicas, así como una convergencia degenerada en una secuencia particular que se repite. También puede haber patrones indeseables. Por ejemplo, suponga que un PRNG parece bastante aleatorio cuando los números se imprimen en decimal, pero una inspección de los valores en binario muestra que el bit 4 simplemente alterna entre 0 y 1 en cada llamada. ¡Uy!

Eche un vistazo al Mersenne Twister y otros algoritmos. Hay formas de lograr un equilibrio entre la duración del período y los ciclos de la CPU. Un enfoque básico (utilizado en el Mersenne Twister) es circular en el vector de estado. Es decir, cuando se genera un número, no se basa en todo el estado, solo en unas pocas palabras del conjunto de estados sujeto a algunas operaciones de bits. Pero en cada paso, el algoritmo también se mueve en la matriz, mezclando los contenidos poco a poco.

Kaz
fuente
55
Principalmente estoy de acuerdo, excepto con su primer párrafo. Las llamadas aleatorias integradas (y / dev / random en sistemas similares a Unix) también son PRNG. Llamaría a cualquier cosa que produzca números aleatorios algorítmicamente un PRNG, incluso si la semilla es algo difícil de predecir. Existen algunos generadores de números aleatorios "verdaderos" que utilizan la desintegración radiactiva, el ruido atmosférico, etc., pero a menudo generan relativamente pocos bits / segundo.
Matt Krause
En los cuadros de Linux, /dev/randomes una fuente de aleatoriedad real obtenida de los controladores de dispositivo, y no un PRNG. Bloquea cuando no hay suficientes bits disponibles. El dispositivo hermano /dev/urandomtampoco bloquea, pero todavía no es exactamente un PRNG ya que se actualiza con bits aleatorios cuando están disponibles.
Kaz
Si la función Math.Random () llama al sistema operativo para obtener la hora del día , esto es absolutamente falso. (en cualquiera de los sabores / versiones de Java que conozco)
bestsss
@bestsss Esto es de la pregunta original: recuerdo haber leído en alguna parte que Math.random usó System.nanoTime () . Puede valer la pena agregar su conocimiento allí o en su respuesta. Lo usé condicionalmente con un if . :)
Kaz
Kaz, ambos nanoTime()+ counter / hash se usan para la semilla predeterminada java.util.Randomde oracle / OpenJDK. Eso es solo para la semilla, entonces es un LCG estándar. En efecto, el generador OP toma 2 números aleatorios para semilla, lo cual está bien, así que no hay diferencia que java.util.Random. System.currentTimeMillis()fue la semilla predeterminada en JDK1.4-
bestsss
7

Hay muchos, muchos generadores de números pseudoaleatorios por ahí. Por ejemplo, el ranarray de Knuth , el tornado de Mersenne o buscar generadores LFSR. Los "algoritmos seminuméricos" monumentales de Knuth analizan el área y proponen algunos generadores congruenciales lineales (fáciles de implementar, rápidos).

Pero te sugiero que te limites a java.util.Randomo Math.random, son rápidos y al menos están bien para uso ocasional (es decir, juegos y demás). Si solo estás paranoico en la distribución (algún programa de Monte Carlo o un algoritmo genético), revisa su implementación (la fuente está disponible en algún lugar) y siembra con algún número verdaderamente aleatorio, ya sea de tu sistema operativo o de random.org . Si esto es necesario para alguna aplicación donde la seguridad es crítica, tendrá que cavar usted mismo. Y como en ese caso, no deberías creer lo que arroja un cuadrado de color con partes faltantes aquí, me callaré ahora.

vonbrand
fuente
7

Es muy poco probable que el rendimiento de la generación de números aleatorios sea un problema para cualquier caso de uso que se le ocurra, a menos que acceda a una sola Randominstancia desde varios subprocesos (porque lo Randomes synchronized).

Sin embargo, si ese es realmente el caso y necesita muchos números aleatorios rápidamente, su solución es demasiado poco confiable. A veces da buenos resultados, a veces da resultados horribles (según la configuración inicial).

Si desea los mismos números que Randomle da la clase, solo que más rápido, puede deshacerse de la sincronización allí:

public class QuickRandom {

    private long seed;

    private static final long MULTIPLIER = 0x5DEECE66DL;
    private static final long ADDEND = 0xBL;
    private static final long MASK = (1L << 48) - 1;

    public QuickRandom() {
        this((8682522807148012L * 181783497276652981L) ^ System.nanoTime());
    }

    public QuickRandom(long seed) {
        this.seed = (seed ^ MULTIPLIER) & MASK;
    }

    public double nextDouble() {
        return (((long)(next(26)) << 27) + next(27)) / (double)(1L << 53);
    }

    private int next(int bits) {
        seed = (seed * MULTIPLIER + ADDEND) & MASK;
        return (int)(seed >>> (48 - bits));
    }

}

Simplemente tomé el java.util.Randomcódigo y eliminé la sincronización que da como resultado el doble de rendimiento en comparación con el original en mi Oracle HotSpot JVM 7u9. Todavía es más lento que tu QuickRandom, pero da resultados mucho más consistentes. Para ser precisos, para los mismos seedvalores y aplicaciones de subproceso único, proporciona los mismos números pseudoaleatorios que la Randomclase original .


Este código se basa en el actual java.util.Randomen OpenJDK 7u, que está licenciado bajo GNU GPL v2 .


EDITAR 10 meses después:

Acabo de descubrir que ni siquiera tiene que usar mi código anterior para obtener una Randominstancia no sincronizada . ¡También hay uno en el JDK!

Mira la ThreadLocalRandomclase de Java 7 . El código dentro de él es casi idéntico a mi código anterior. La clase es simplemente una Randomversión aislada de hilo local adecuada para generar números aleatorios rápidamente. El único inconveniente que se me ocurre es que no puede configurarlo seedmanualmente.

Ejemplo de uso:

Random random = ThreadLocalRandom.current();
Petr Janeček
fuente
2
@Edit Hmm, puedo comparar QR, Math.random y ThreadLocalRandom en algún momento cuando no soy demasiado vago ¡ :)Eso es interesante, gracias!
tckmn
1. Puede ganar más velocidad soltando la máscara, ya que los 16 bits más altos no influyen en los bits utilizados. 2. Puede usar esos bits, guardar una resta y obtener un mejor generador (estado más grande; los bits más significativos de un producto son los que están mejor distribuidos, pero sería necesaria alguna evaluación). 3. Los chicos de Sun simplemente implementaron un arcaico RNG de Knuth y agregaron sincronización. :(
maaartinus
3

'Aleatorio' es más que solo obtener números ... lo que tienes es pseudoaleatorio

Si seudoaleatorio es lo suficientemente bueno para sus propósitos, entonces seguro, es mucho más rápido (y XOR + Bitshift será más rápido de lo que tiene)

Rolf

Editar:

OK, después de ser demasiado apresurado en esta respuesta, déjame responder la verdadera razón por la cual tu código es más rápido:

Desde JavaDoc para Math.Random ()

Este método está sincronizado adecuadamente para permitir el uso correcto de más de un hilo. Sin embargo, si muchos hilos necesitan generar números pseudoaleatorios a una gran velocidad, puede reducir la contención para que cada hilo tenga su propio generador de números pseudoaleatorios.

Esto es probable por qué su código es más rápido.

rolfl
fuente
3
Prácticamente todo lo que no implique un generador de ruido de hardware o una línea directa en las cosas de E / S del sistema operativo será pseudoaleatorio. La aleatoriedad genuina no puede ser generada solo por un algoritmo; Necesitas ruido de algún lado. (RNG algunos Sistemas Operativos obtienen su entrada al medir cosas como cómo / cuando se mueve el ratón, el tipo de material, etc. medido en una escala de microsegundos a nanosegundos, que puede ser muy impredecible.)
Chao
@OliCharlesworth: de hecho, hasta donde yo sé, los únicos valores aleatorios verdaderos se encuentran utilizando el ruido atmosférico.
Jeroen Vannevel
@me ... estúpido para responder apresuradamente. Math.random es pseudoaleatorio y, además, está sincronizado .
rolfl
@rolfl: La sincronización podría explicar muy bien por qué Math.random()es más lenta. Tendría que sincronizar o crear uno nuevo Randomcada vez, y ninguno de los dos es muy atractivo en términos de rendimiento. Si me importara el rendimiento, crearía el mío new Randomy lo usaría. : P
cHao
La desintegración radiactiva de @JeroenVannevel también es aleatoria.
RxS
3

java.util.Random no es muy diferente, un LCG básico descrito por Knuth. Sin embargo, tiene las 2 principales ventajas / diferencias principales:

  • seguridad para subprocesos: cada actualización es un CAS que es más costoso que una simple escritura y necesita una rama (incluso si se ha predicho perfectamente un subproceso único). Dependiendo de la CPU, podría ser una diferencia significativa.
  • estado interno no revelado: esto es muy importante para cualquier cosa no trivial. Desea que los números aleatorios no sean predecibles.

Debajo está la rutina principal que genera enteros 'aleatorios' en java.util.Random.


  protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
          oldseed = seed.get();
          nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }

Si elimina el AtomicLong y el estado no revelado (es decir, utilizando todos los bits del long), obtendría más rendimiento que la doble multiplicación / módulo.

Última nota: Math.randomno debe usarse para nada más que pruebas simples, es propenso a disputas y si tiene incluso un par de hilos que lo llaman simultáneamente, el rendimiento se degrada. Una característica histórica poco conocida es la introducción de CAS en Java: para superar un punto de referencia infame (primero por IBM a través de intrínsecos y luego Sun hizo "CAS de Java")

bestsss
fuente
0

Esta es la función aleatoria que uso para mis juegos. Es bastante rápido y tiene una buena distribución (suficiente).

public class FastRandom {

    public static int randSeed;

      public static final int random()
      {
        // this makes a 'nod' to being potentially called from multiple threads
        int seed = randSeed;

        seed    *= 1103515245;
        seed    += 12345;
        randSeed = seed;
        return seed;
      }

      public static final int random(int range)
      {
        return ((random()>>>15) * range) >>> 17;
      }

      public static final boolean randomBoolean()
      {
         return random() > 0;
      }

       public static final float randomFloat()
       {
         return (random()>>>8) * (1.f/(1<<24));
       }

       public static final double randomDouble() {
           return (random()>>>8) * (1.0/(1<<24));
       }
}
Terje
fuente
1
Esto no proporciona una respuesta a la pregunta. Para criticar o solicitar una aclaración de un autor, deje un comentario debajo de su publicación.
John Willemse
¿Creo que ya se estableció que el algoritmo original no es lo suficientemente bueno? ¿Quizás un ejemplo de lo que es lo suficientemente bueno puede conducir a la inspiración sobre cómo mejorarlo?
Terje
Sí, tal vez, pero no responde a la pregunta en absoluto y no hay datos que respalden su algoritmo en realidad es "lo suficientemente bueno". En general, los algoritmos de números aleatorios y los algoritmos de cifrado estrechamente relacionados nunca son tan buenos como los de los expertos que los implementaron en un lenguaje de programación. Entonces, si pudiera respaldar su reclamo y explicar por qué es mejor que el algoritmo en la Pregunta, al menos respondería a una pregunta.
John Willemse
Bueno ... Los expertos que los implementaron en un lenguaje de programación buscan una distribución "perfecta", mientras que en un juego, nunca necesitas eso. Desea velocidad y distribución "suficientemente buena". Este código ofrece esto. Si no es apropiado aquí, eliminaré la respuesta, no hay problema.
Terje
Con respecto al subprocesamiento múltiple, su uso de la variable local es un no-op, ya que sin volatileel compilador es libre de eliminar (o introducir) las variables locales a voluntad.
maaartinus