Esse é um algoritmo aleatório “suficientemente bom”; por que não é usado se é mais rápido?

171

Eu fiz uma aula chamada QuickRandom, e seu trabalho é produzir números aleatórios rapidamente. É realmente simples: basta pegar o valor antigo, multiplicar por a doublee pegar a parte decimal.

Aqui está minha QuickRandomturma na íntegra:

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

}

E aqui está o código que escrevi para testá-lo:

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

É um algoritmo muito simples que simplesmente multiplica o dobro anterior por um "número mágico" duplo. Eu juntei tudo rapidamente, então provavelmente poderia melhorar, mas estranhamente, parece estar funcionando bem.

Esta é uma saída de amostra das linhas comentadas no 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

Hum. Bastante aleatório. De fato, isso funcionaria para um gerador de números aleatórios em um jogo.

Aqui está um exemplo de saída da parte não comentada:

5456313909
1427223941

Uau! Ele executa quase 4 vezes mais rápido que Math.random.

Lembro-me de ler em algum lugar que Math.randomusava System.nanoTime()e toneladas de módulos malucos e coisas de divisão. Isso é mesmo necessário? Meu algoritmo executa muito mais rápido e parece bastante aleatório.

Eu tenho duas perguntas:

  • Meu algoritmo é "bom o suficiente" (para, digamos, um jogo, onde números realmente aleatórios não são muito importantes)?
  • Por que Math.randomfazer tanto quando parece simples multiplicação e cortar o decimal é suficiente?
tckmn
fonte
154
"parece bem aleatório"; você deve gerar um histograma e executar alguns autocorrelação em sua seqüência ...
Oliver Charlesworth
63
Ele quer dizer que "parece bastante aleatório" não é realmente uma medida objetiva da aleatoriedade e você deve obter algumas estatísticas reais.
Matt H
23
@ Doorknob: Em termos de leigos, você deve investigar se seus números têm uma distribuição "plana" entre 0 e 1 e verificar se há algum padrão periódico / repetitivo ao longo do tempo.
precisa
22
Tente new QuickRandom(0,5)ou new QuickRandom(.5, 2). Ambos produzirão repetidamente 0 para o seu número.
FrankieTheKneeMan
119
Escrever seu próprio algoritmo de geração de número aleatório é como escrever seu próprio algoritmo de criptografia. Há tanta arte anterior, por pessoas hiper-qualificadas, que não faz sentido gastar o seu tempo tentando acertar. Não há razão para não usar as funções da biblioteca Java e, se você realmente quiser criar suas próprias, por algum motivo, visite a Wikipedia e procure algoritmos como o Mersenne Twister.
Steveha

Respostas:

351

Sua QuickRandomimplementação não tem realmente uma distribuição uniforme. As frequências são geralmente mais altas nos valores mais baixos, enquanto Math.random()tem uma distribuição mais uniforme. Aqui está um SSCCE que mostra 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));
        }
    }

}

O resultado médio é assim:

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  :###################    

Se você repetir o teste, verá que a distribuição QR varia muito, dependendo das sementes iniciais, enquanto a distribuição MR é estável. Às vezes, atinge a distribuição uniforme desejada, mas mais do que frequentemente não. Aqui está um dos exemplos mais extremos: está além dos limites do 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
fonte
17
+1 para dados numéricos - embora a observação de números brutos possa ser enganosa, pois não significa que eles tenham diferença estatisticamente significativa.
Maciej Piechotka
16
Esses resultados variam muito com as sementes iniciais passadas para QuickRandom. Às vezes, é quase uniforme, às vezes é muito pior que isso.
Petr Janeček 24/01
68
@ BlueRaja-DannyPflughoeft Qualquer PRNG em que a qualidade da saída dependa muito dos valores iniciais de sementes (em oposição às constantes internas) parece quebrado para mim.
um CVn
22
Primeira regra de estatística: plote os dados . Sua análise é direta, mas a plotagem de um histograma mostra isso muito mais rapidamente. ;-) (E é duas linhas em R.)
Konrad Rudolph
37
Citações obrigatórias: "Qualquer pessoa que considere métodos aritméticos de produção de dígitos aleatórios está, é claro, em estado de pecado". - John von Neumann (1951) "Quem nunca viu a citação acima em pelo menos 100 lugares provavelmente não é muito velho." - DV Pryor (1993) “Geradores de números aleatórios não devem ser escolhidos aleatoriamente.” - Donald Knuth (1986)
Happy Green Kid Naps
133

O que você está descrevendo é um tipo de gerador aleatório chamado gerador congruencial linear . O gerador funciona da seguinte maneira:

  • Comece com um valor inicial e multiplicador.
  • Para gerar um número aleatório:
    • Multiplique a semente pelo multiplicador.
    • Defina a semente igual a este valor.
    • Retorne esse valor.

Este gerador tem muitas propriedades agradáveis, mas tem problemas significativos como uma boa fonte aleatória. O artigo da Wikipedia vinculado acima descreve alguns dos pontos fortes e fracos. Em resumo, se você precisar de bons valores aleatórios, essa provavelmente não é uma abordagem muito boa.

Espero que isto ajude!

templatetypedef
fonte
@ louism- Não é realmente "aleatório", por si só. Os resultados serão determinísticos. Dito isto, não pensei nisso ao escrever minha resposta; talvez alguém possa esclarecer esse detalhe?
templatetypedef
2
Erros aritméticos de ponto flutuante são projetados para implementação. Tanto quanto eu sei, eles são consistentes para uma determinada plataforma, mas podem diferir, por exemplo, entre diferentes telefones celulares e entre arquiteturas de PC. Embora algumas vezes sejam adicionados bits de guarda extras ao fazer uma série de cálculos de ponto flutuante em uma linha, e a presença ou ausência desses bits de guarda pode fazer com que um cálculo diferencie sutilmente o resultado. (bits de guarda sendo, por exemplo, a expansão de um 64 bit duplo de 80 bits)
Patashu
2
Além disso, lembre-se de que a teoria por trás dos LCRNGs supõe que você esteja trabalhando com números inteiros! Jogar números de ponto flutuante nele não produzirá a mesma qualidade de resultados.
você precisa saber é o seguinte
1
@duskwuff, você está certo. Mas se o hardware de ponto flutuante segue regras sãs, isso é o mesmo que modular o tamanho da mantissa, e a teoria se aplica. Só precisa de cuidados extras no que você está fazendo.
vonbrand
113

Sua função de número aleatório é ruim, pois possui muito pouco estado interno - o número de saída da função em qualquer etapa é totalmente dependente do número anterior. Por exemplo, se assumirmos que magicNumberé 2 (a título de exemplo), a sequência:

0.10 -> 0.20

é fortemente espelhado por sequências semelhantes:

0.09 -> 0.18
0.11 -> 0.22

Em muitos casos, isso gerará correlações visíveis em seu jogo - por exemplo, se você fizer chamadas sucessivas para sua função para gerar coordenadas X e Y para objetos, os objetos formarão padrões diagonais claros.

A menos que você tenha boas razões para acreditar que o gerador de números aleatórios está atrasando seu aplicativo (e isso é MUITO improvável), não há boas razões para tentar escrever por conta própria.

duskwuff -inactive-
fonte
36
+1 para uma resposta prática ... usar isso em um tiro em cima e gerar inimigos ao longo das diagonais para vários tiros épicos na cabeça? : D
wim
@ wim: você não precisa de um PRNG se quiser esses padrões.
Lie Ryan
109

O verdadeiro problema disso é que o histograma de saída depende muito da semente inicial - na maioria das vezes acabará com uma saída quase uniforme, mas na maioria das vezes terá uma saída distintamente não uniforme.

Inspirado neste artigo sobre o quão ruim é a rand()função do php, criei algumas imagens de matriz aleatória usando QuickRandome System.Random. Esta execução mostra como às vezes a semente pode ter um efeito ruim (neste caso, favorecendo números mais baixos), onde System.Randomé bastante uniforme.

QuickRandom

System.Random

Pior ainda

Se inicializarmos QuickRandomconforme new QuickRandom(0.01, 1.03)obtemos esta imagem:

O 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
fonte
2
Código legal. Sim, isso é legal. Eu costumava fazer isso também algumas vezes, é difícil obter uma medida quantificável, mas é outra boa maneira de analisar a sequência. E se você quiser dar uma olhada em sequências maiores que a largura * altura, você pode definir a próxima imagem com esse pixel por pixel. Eu acho que a imagem do QuickRandom é muito mais esteticamente agradável, por ser texturizada como um tapete de algas marinhas.
Cris Stringfellow
A parte esteticamente agradável é como a sequência tende a aumentar à medida que você avança em cada linha (e depois volta ao início novamente), pois a magicNumbermultiplicação produz um número semelhante a prevNum, o que mostra a falta de aleatoriedade. Se usarmos as sementes new QuickRandom(0.01, 1.03), obtemos este i.imgur.com/Q1Yunbe.png !
Callum Rogers
Sim, ótima análise. Como ele apenas multiplica o mod 1 por uma constante claramente antes de ocorrer a quebra, haverá o aumento que você descreve. Parece que isso poderia ser evitado se adotássemos as colocações decimais menos significativas, multiplicando por 1 bilhão e reduzindo mod uma paleta de 256 cores.
precisa saber é o seguinte
Você pode me dizer o que você usou para gerar essas imagens de saída? Matlab?
uday 30/01
@uDaY: Dê uma olhada no código, C # e System.Drawing.Bitmap.
Callum Rogers
37

Um problema com seu gerador de números aleatórios é que não existe um 'estado oculto' - se eu souber qual número aleatório você retornou na última chamada, conheço todos os números aleatórios que você enviará até o final dos tempos, pois existe apenas um possível próximo resultado, e assim por diante.

Outra coisa a considerar é o 'período' do seu gerador de números aleatórios. Obviamente, com um tamanho de estado finito, igual à porção mantissa de um duplo, ele só poderá retornar no máximo 2 ^ 52 valores antes do loop. Mas esse é o melhor caso - você pode provar que não há loops do período 1, 2, 3, 4 ...? Se houver, seu RNG terá um comportamento terrível e degenerado nesses casos.

Além disso, sua geração de números aleatórios terá uma distribuição uniforme para todos os pontos de partida? Caso contrário, seu RNG será tendencioso - ou pior, tendencioso de maneiras diferentes, dependendo da semente inicial.

Se você pode responder a todas essas perguntas, incrível. Se você não pode, então você sabe por que a maioria das pessoas não reinventa a roda e usa um gerador de números aleatórios comprovado;)

(A propósito, um bom ditado é: O código mais rápido é o código que não é executado. Você pode fazer o aleatório mais rápido () do mundo, mas não é bom se não for muito aleatório)

Patashu
fonte
8
Há pelo menos um ciclo trivial sobre este gerador para todas as sementes: 0 -> 0. Dependendo da semente, pode haver muitas outras. (Por exemplo, com uma semente 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

Um teste comum que sempre fiz ao desenvolver PRNGs foi:

  1. Converter saída em valores de caracteres
  2. Grave o valor dos caracteres em um arquivo
  3. Compactar arquivo

Isso permitiu que eu iterasse rapidamente em idéias que eram PRNGs "suficientemente boas" para seqüências de 1 a 20 megabytes. Ele também forneceu uma imagem de cima para baixo melhor do que apenas examiná-la a olho, pois qualquer PRNG "bom o suficiente" com meia palavra de estado poderia rapidamente exceder a capacidade dos olhos de ver o ponto do ciclo.

Se eu fosse realmente exigente, poderia pegar os bons algoritmos e executar os testes DIEHARD / NIST neles, para obter mais informações e depois voltar e ajustar um pouco mais.

A vantagem do teste de compressão, ao contrário de uma análise de frequência, é que, trivialmente, é fácil construir uma boa distribuição: basta gerar um bloco de 256 comprimentos contendo todos os caracteres dos valores de 0 a 255 e fazer isso 100.000 vezes. Mas essa sequência tem um ciclo de comprimento 256.

Uma distribuição distorcida, mesmo que por uma pequena margem, deve ser captada por um algoritmo de compactação, principalmente se você fornecer o suficiente (digamos 1 megabyte) da sequência para trabalhar. Se alguns caracteres, bigrams ou n-gramas ocorrerem com mais frequência, um algoritmo de compactação pode codificar essa inclinação da distribuição para códigos que favorecem as ocorrências frequentes com palavras de código mais curtas e você obtém um delta de compactação.

Como a maioria dos algoritmos de compactação é rápida e não requer implementação (já que os SOs os estão por aí), o teste de compactação é muito útil para classificar rapidamente a aprovação / reprovação de um PRNG que você possa estar desenvolvendo.

Boa sorte com seus experimentos!

Ah, eu realizei esse teste na rng que você tem acima, usando o seguinte mod pequeno do seu 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));
        }
    }
}

Os resultados foram:

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

Eu consideraria um PRNG bom se o arquivo de saída não pudesse ser compactado. Para ser honesto, não achei que o seu PRNG se sairia tão bem, apenas 16% em ~ 20 Megs é bastante impressionante para uma construção tão simples. Mas ainda considero um fracasso.

Cris Stringfellow
fonte
2
Imaginando isso ou não, tenho a mesma idéia com o zip anos atrás, quando testo meus geradores aleatórios.
Aristos
1
Obrigado @Alexandre C. e Aristos e aidan. Eu acredito em você.
precisa saber é o seguinte
33

O gerador aleatório mais rápido que você pode implementar é este:

insira a descrição da imagem aqui

XD, brinca à parte, além de tudo o que foi dito aqui, eu gostaria de contribuir citando que testar seqüências aleatórias "é uma tarefa difícil" [1], e existem vários testes que verificam certas propriedades de números pseudo-aleatórios. muitos deles aqui: http://www.random.org/analysis/#2005

Uma maneira simples de avaliar a "qualidade" do gerador aleatório é o antigo teste do qui quadrado.

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

Citação [1]

A idéia do teste do χ² é verificar se os números produzidos estão ou não distribuídos razoavelmente. Se gerarmos N números positivos menores que r , esperamos obter N / r de cada valor. Mas --- e esta é a essência da questão --- as frequências de ocorrência de todos os valores não devem ser exatamente as mesmas: isso não seria aleatório!

Simplesmente calculamos a soma dos quadrados das freqüências de ocorrência de cada valor, escalados pela frequência esperada e subtraímos o tamanho da sequência. Esse número, a "estatística χ²", pode ser expresso matematicamente como

fórmula qui-quadrado

Se a estatística χ² estiver próxima de r , os números serão aleatórios; se estiver muito longe, eles não estarão. As noções de "fechar" e "longe" podem ser definidas com mais precisão: existem tabelas que mostram exatamente como relacionar a estatística às propriedades de seqüências aleatórias. Para o teste simples que estamos realizando, a estatística deve estar dentro de 2√r

Usando essa teoria e o seguinte 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);
    }
}

Obtive o seguinte resultado:

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

Que, para o QuickRandom, está longe de r (fora de r ± 2 * sqrt(r))

Dito isto, o QuickRandom pode ser rápido, mas (como indicado em outras respostas) não é bom como um gerador de números aleatórios


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

higuaro
fonte
9
+1 para xkcd, que é um incrível site deob (oh, e a grande resposta): P
tckmn
1
Obrigado, e sim racks xkcd! XD
higuaro
A teoria é boa, mas a execução é ruim: o código é suscetível ao estouro de números inteiros. No java, todos int[]são inicializados em zero, portanto, não há necessidade dessa parte. A conversão para flutuar não faz sentido quando você trabalha com dobra. Por último: chamar os nomes de métodos random1 e random2 é bem engraçado.
bestsss 29/01
@bestsss Obrigado pelas observações! Fiz uma tradução direta do código C e não prestei muita atenção a ela = (I fez algumas modificações e atualizado a resposta eu apreciaria qualquer sugestão adicional..
higuaro
14

Eu montei uma rápida maquete do seu algoritmo em JavaScript para avaliar os resultados. Ele gera 100.000 números inteiros aleatórios de 0 a 99 e rastreia a instância de cada número inteiro.

A primeira coisa que noto é que é mais provável que você obtenha um número baixo do que um número alto. Você vê isso mais quando seed1está alto e seed2baixo. Em alguns casos, obtive apenas três números.

Na melhor das hipóteses, seu algoritmo precisa de algum aprimoramento.

gilly3
fonte
8

Se a Math.Random()função chamar o sistema operacional para obter a hora do dia, você não poderá compará-lo à sua função. Sua função é um PRNG, enquanto essa função está buscando números aleatórios reais. Maçãs e laranjas.

Seu PRNG pode ser rápido, mas não possui informações de estado suficientes para atingir um longo período antes da repetição (e sua lógica não é sofisticada o suficiente para atingir os períodos possíveis com tantas informações de estado).

Período é a duração da sequência antes que o seu PRNG comece a se repetir. Isso acontece assim que a máquina PRNG faz uma transição de estado para um estado que é idêntico a algum estado passado. A partir daí, ele repetirá as transições que começaram nesse estado. Outro problema com os PRNG pode ser um número baixo de sequências únicas, bem como convergência degenerada em uma sequência específica que se repete. Também pode haver padrões indesejáveis. Por exemplo, suponha que um PRNG pareça bastante aleatório quando os números são impressos em decimal, mas uma inspeção dos valores em binário mostra que o bit 4 está simplesmente alternando entre 0 e 1 em cada chamada. Opa!

Dê uma olhada no Mersenne Twister e outros algoritmos. Existem maneiras de encontrar um equilíbrio entre a duração do período e os ciclos da CPU. Uma abordagem básica (usada no Mersenne Twister) é percorrer o vetor de estado. Ou seja, quando um número está sendo gerado, ele não se baseia em todo o estado, apenas em algumas palavras da matriz de estados, sujeitas a operações de alguns bits. Mas a cada passo, o algoritmo também se move na matriz, embaralhando o conteúdo um pouco de cada vez.

Kaz
fonte
5
Eu concordo principalmente, exceto com o seu primeiro parágrafo. As chamadas aleatórias integradas (e / dev / random em sistemas do tipo Unix) também são PRNGs. Eu chamaria qualquer coisa que produza números aleatórios algoritmicamente de PRNG, mesmo que a semente seja algo difícil de prever. Existem alguns geradores de números aleatórios "verdadeiros" que usam decaimento radioativo, ruído atmosférico etc., mas geralmente geram relativamente poucos bits / segundo.
Matt Krause
Nas caixas Linux, /dev/randomé uma fonte de aleatoriedade real obtida de drivers de dispositivo, e não um PRNG. Ele bloqueia quando não há bits suficientes disponíveis. O dispositivo irmão /dev/urandomtambém não bloqueia, mas ainda não é exatamente um PRNG, pois é atualizado com bits aleatórios quando estão disponíveis.
Kaz
Se a função Math.Random () chamar o sistema operacional para obter a hora do dia - isso é absolutamente falso. (em qualquer um dos java sabores / versões eu saiba)
bestsss
@bestsss Isto é da pergunta original: Lembro-me de ler em algum lugar que Math.random usava System.nanoTime () . Vale a pena adicionar seu conhecimento lá ou em sua resposta. Eu usei condicionalmente com um if . :)
Kaz
Kaz, ambos nanoTime()+ contador / hash são usados ​​para a semente padrão java.util.Randomdo oracle / OpenJDK. Isso é apenas para a semente, então é um LCG padrão. De fato, o gerador OP utiliza 2 números aleatórios para a semente, o que é bom - então não há diferença java.util.Random. System.currentTimeMillis()foi a semente padrão no JDK1.4-
bestsss 25/01
7

Existem muitos, muitos geradores de números pseudo-aleatórios por aí. Por exemplo, a matriz de Knuth , a Mersenne twister , ou procure geradores LFSR. O monumental "Algoritmos Seminuméricos" de Knuth analisa a área e propõe alguns geradores congruenciais lineares (simples de implementar, rápidos).

Mas eu sugiro que você se atenha ao java.util.Randomor Math.random, eles são rápidos e pelo menos aceitáveis ​​para uso ocasional (por exemplo, jogos e coisas assim). Se você é paranóico na distribuição (algum programa de Monte Carlo ou um algoritmo genético), verifique a implementação deles (a fonte está disponível em algum lugar) e propague-os com um número verdadeiramente aleatório, no sistema operacional ou no random.org . Se isso for necessário para algum aplicativo em que a segurança é crítica, você terá que se aprofundar. E, como nesse caso, você não deve acreditar no que algum quadrado colorido com os bits faltando vaza aqui, vou calar a boca agora.

vonbrand
fonte
7

É muito improvável que o desempenho da geração de números aleatórios seja um problema para qualquer caso de uso que você tenha, a menos que acesse uma única Randominstância de vários encadeamentos (porque Randomé synchronized).

No entanto, se esse for realmente o caso e você precisar de muitos números aleatórios rapidamente, sua solução não será confiável. Às vezes, dá bons resultados, às vezes, resultados horríveis (com base nas configurações iniciais).

Se você deseja os mesmos números que a Randomclasse fornece, apenas mais rapidamente, você pode se livrar da sincronização:

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

}

Simplesmente peguei o java.util.Randomcódigo e removi a sincronização, que resulta no dobro do desempenho em comparação com o original no meu Oracle HotSpot JVM 7u9. Ainda é mais lento que o seu QuickRandom, mas fornece resultados muito mais consistentes. Para ser preciso, para os mesmos seedvalores e aplicativos de encadeamento único, ele fornece os mesmos números pseudo-aleatórios da Randomclasse original .


Este código é baseado na corrente java.util.Randomno OpenJDK 7u, licenciada sob o GNU GPL v2 .


EDIT 10 meses depois:

Acabei de descobrir que você nem precisa usar meu código acima para obter uma Randominstância não sincronizada . Também há um no JDK!

Veja a ThreadLocalRandomclasse do Java 7 . O código dentro dele é quase idêntico ao meu código acima. A classe é simplesmente uma Randomversão isolada de encadeamento local, adequada para gerar números aleatórios rapidamente. A única desvantagem em que consigo pensar é que você não pode defini-la seedmanualmente.

Exemplo de uso:

Random random = ThreadLocalRandom.current();
Petr Janeček
fonte
2
@Edit Hmm, posso comparar QR, Math.random e ThreadLocalRandom em algum momento em que não tenha muita preguiça :)Isso é interessante, obrigado!
usar o seguinte comando
1. Você pode ganhar mais velocidade soltando a máscara, pois os 16 bits mais altos não influenciam os bits usados. 2. Você pode usar esses bits, salvar uma subtração e obter um gerador melhor (estado maior; os bits mais significativos de um produto são os mais bem distribuídos, mas alguma avaliação seria necessária). 3. Os caras da Sun simplesmente implementaram um RNG arcaico de Knuth e adicionaram sincronização. :(
maaartinus 12/06
3

'Aleatório' é mais do que apenas obter números ... o que você tem é pseudo-aleatório

Se o pseudo-aleatório for bom o suficiente para seus propósitos, então é muito mais rápido (e o XOR + Bitshift será mais rápido do que você tem)

Rolf

Editar:

OK, depois de ser muito apressado nesta resposta, deixe-me responder a verdadeira razão pela qual seu código é mais rápido:

No JavaDoc for Math.Random ()

Este método está sincronizado corretamente para permitir o uso correto por mais de um thread. No entanto, se muitos encadeamentos precisarem gerar números pseudo-aleatórios a uma grande velocidade, isso poderá reduzir a contenção de cada encadeamento para ter seu próprio gerador de número pseudo-aleatório.

É provavelmente por isso que seu código é mais rápido.

rolfl
fonte
3
Praticamente qualquer coisa que não envolva um gerador de ruído de hardware ou uma linha direta nas coisas de E / S do sistema operacional será pseudo-aleatória. A aleatoriedade genuína não pode ser gerada apenas por um algoritmo; você precisa de barulho de algum lugar. (Os RNGs de alguns sistemas operacionais obtêm sua entrada medindo itens como / quando você move o mouse, digitando itens etc. Medidos em uma escala de microssegundos a nanossegundos, que podem ser altamente imprevisíveis.)
cHao
@OliCharlesworth: na verdade, até onde eu sei, os únicos valores aleatórios verdadeiros são encontrados usando o ruído atmosférico.
Jeroen Vannevel
@me ... estúpido para responder às pressas. O Math.random é pseudo-aleatório e também é sincronizado .
rolfl
@rolfl: A sincronização poderia muito bem explicar por que Math.random()é mais lenta. Teria que sincronizar ou criar um novo Randomtodas as vezes, e nenhum deles seria muito atraente em termos de desempenho. Se eu me importasse com o desempenho, criaria o meu próprio new Randome apenas o usaria. : P
cHao 24/01
O decaimento radioativo @JeroenVannevel também é aleatório.
RXS
3

O java.util.Random não é muito diferente, um LCG básico descrito por Knuth. No entanto, possui as 2 principais vantagens / diferenças principais:

  • thread safe - cada atualização é um CAS que é mais caro que uma simples gravação e precisa de uma ramificação (mesmo que seja perfeitamente previsto com thread único). Dependendo da CPU, pode haver uma diferença significativa.
  • estado interno não revelado - isso é muito importante para qualquer coisa não trivial. Você deseja que os números aleatórios não sejam previsíveis.

Abaixo está a rotina principal que gera números inteiros 'aleatórios' em 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));
    }

Se você remover o AtomicLong e o estado não revelado (ou seja, usando todos os bits do long), obterá mais desempenho do que a multiplicação / módulo duplo.

Última observação: Math.randomnão deve ser usado para nada além de testes simples, é propenso a contendas e, se você tem alguns threads chamando simultaneamente, o desempenho diminui. Uma característica histórica pouco conhecida é a introdução do CAS em java - para superar uma referência infame (primeiro pela IBM via intrínseca e depois pela Sun criou o "CAS from Java")

bestsss
fonte
0

Esta é a função aleatória que uso nos meus jogos. É bem rápido e tem boa distribuição (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
fonte
1
Isso não fornece uma resposta para a pergunta. Para criticar ou solicitar esclarecimentos a um autor, deixe um comentário abaixo da postagem.
precisa
Eu acho que já foi estabelecido que o algoritmo original não é bom o suficiente? Talvez um exemplo do que seja bom o suficiente possa levar à inspiração de como melhorá-lo?
Terje
Sim, talvez, mas não responde à pergunta e não há dados que suportem seu algoritmo, na verdade, é "bom o suficiente". Geralmente, algoritmos de números aleatórios e algoritmos de criptografia intimamente relacionados nunca são tão bons quanto os dos especialistas que os implementaram em uma linguagem de programação. Portanto, se você pudesse apoiar sua reivindicação e explicar por que ela é melhor do que o algoritmo da pergunta, você responderia pelo menos a uma pergunta.
precisa
Bem ... Especialistas que os implementaram em uma linguagem de programação visam uma distribuição "perfeita", enquanto em um jogo você nunca precisa disso. Você quer velocidade e distribuição "suficientemente boa". Este código oferece isso. Se não for adequado aqui, excluirei a resposta, sem problemas.
Terje
No que diz respeito à multithreading, seu uso da variável local é um não operacional, pois sem volatileo compilador é livre para eliminar (ou introduzir) variáveis ​​locais à vontade.
Maaartinus