这是“足够好”的随机算法吗?如果速度更快,为什么不使用它呢?


171

我做了一个叫的类QuickRandom,它的工作是快速产生随机数。这很简单:只取旧值,乘以a double,然后取小数部分。

这是我QuickRandom的全部课程:

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

这是一种非常简单的算法,只需将先前的双精度数乘以“幻数”双精度数即可。我很快将它放到一起,所以我可能会做得更好,但是奇怪的是,它似乎运行良好。

这是该main方法中注释掉的行的示例输出:

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

嗯 相当随机。实际上,这将适用于游戏中的随机数生成器。

这是未注释掉部分的示例输出:

5456313909
1427223941

哇!它的执行速度几乎是的4倍Math.random

我记得读书的地方,Math.random使用System.nanoTime()和吨的疯狂系数和分裂的东西。那真的有必要吗?我的算法执行速度快很多,而且看起来很随机。

我有两个问题:

  • 我的算法是否足够好(例如,对于一个游戏,实际上随机数不太重要)?
  • 为什么Math.random在看起来只是简单的乘法并舍去小数就足够了的情况下做那么多呢?

154
“似乎很随意”;您应该生成一个直方图并在序列上运行一些自相关...
Oliver Charlesworth

63
他的意思是“似乎相当随机”实际上不是客观的随机性度量,您应该获得一些实际的统计数据。
Matt H

23
@Doorknob:用外行的话来说,您应该调查您的数字在0到1之间是否具有“平坦”分布,并观察一段时间内是否存在任何周期性/重复性模式。
奥利弗·查尔斯沃思

22
尝试new QuickRandom(0,5)new QuickRandom(.5, 2)。这些都将重复输出0为您的数字。
FrankieTheKneeMan 2013年

119
编写自己的随机数生成算法就像编写自己的加密算法。资格很高的人有太多的现有技术,以至于花时间试图弄清楚它是没有意义的。没有理由不使用Java库函数,如果您出于某些原因确实想编写自己的Java库函数,请访问Wikipedia并在其中查找诸如Mersenne Twister之类的算法。
steveha,

Answers:


351

您的QuickRandom实现实际上并不是一个统一的分布。频率通常在较低值时较高,而Math.random()分布更均匀。这是一个SSCCE,它表明:

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

}

平均结果如下所示:

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

如果重复测试,您会发现QR分布变化很大,具体取决于初始种子,而MR分布稳定。有时,它会达到所需的均匀分布,但更多的是没有达到。这是更极端的例子之一,它甚至超出了图表的边界:

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  :                                                  

17
数字数据为+1-尽管查找原始数字可能会产生误导,因为这并不意味着它们在统计上有显着差异。
Maciej Piechotka

16
这些结果与传递给的初始种子有很大差异QuickRandom。有时,它是接近一致的,有时是很多比这更糟糕。
PetrJaneček13年

68
@ BlueRaja-DannyPflughoeft任何PRNG的输出质量在很大程度上取决于初始种子值(而不是内部常数)对我来说似乎都是坏的。
CVn

22
统计的第一法则:绘制数据。您的分析是即时的,但是绘制直方图可以更快地显示出来。;-)(R中有两行)
康拉德·鲁道夫

37
强制性引语:“任何考虑产生随机数字的算术方法的人,当然都处于犯罪状态。” -约翰·冯·诺依曼(John von Neumann,1951年)“任何在至少100个地方都没有看到上述报价的人可能年龄不大。” -DV Pryor(1993)“不应随机选择随机数生成器。” -唐纳德​​·努斯(Donald Knuth)(1986)
快乐的小孩子午睡

133

您所描述的是一种随机生成器,称为线性同余生成器。生成器的工作方式如下:

  • 从种子值和乘数开始。
  • 生成随机数:
    • 将种子乘以乘数。
    • 设置种子等于该值。
    • 返回此值。

该生成器具有许多不错的属性,但是作为良好的随机源却存在很多问题。上面链接的Wikipedia文章描述了一些优点和缺点。简而言之,如果您需要良好的随机值,那么这可能不是一个很好的方法。

希望这可以帮助!


@ louism-本身并不是真正的“随机”。结果将是确定的。就是说,我在写答案时并没有考虑这一点。也许有人可以澄清这个细节?
templatetypedef

2
浮点算术错误是实现设计的。据我所知,它们在特定平台上是一致的,但在不同的手机之间以及PC体系结构之间可能有所不同。尽管在连续进行一系列浮点计算时有时会添加额外的“保护位”,并且这些保护位的存在与否可能会使计算结果有所不同。(保护位例如是将64位的两倍扩展到80位)
Patashu

2
另外,请记住,LCRNG背后的理论都假设您正在使用整数!向其抛出浮点数将不会产生相同质量的结果。
duskwuff -inactive-

1
@duskwuff,你是对的。但是,如果浮点硬件确实遵循理智的规则,则此操作与对尾数大小取模的操作相同,并且该理论也适用。只需要在做事时多加注意。
vonbrand 2013年

113

您的随机数函数很差,因为它的内部状态太少了-该函数在任何给定步骤输出的数字完全取决于先前的数字。例如,如果我们假设它magicNumber是2(作为示例),那么序列:

0.10 -> 0.20

类似序列强烈地反映了这一点:

0.09 -> 0.18
0.11 -> 0.22

在许多情况下,这会在游戏中产生明显的相关性-例如,如果连续调用函数以生成对象的X和Y坐标,则对象将形成清晰的对角线图案。

除非您有充分的理由相信随机数生成器会降低您的应用程序运行速度(这是极不可能的),否则没有充分的理由尝试编写自己的应用程序。


36
+1获得实用答案...在射击过程中使用此功能并沿对角线生成敌人以获得史诗般的多次爆头?:D
2013年

@wim:如果您想要这样的模式,则不需要PRNG。
Lie Ryan

109

真正的问题是它的输出直方图在很大程度上取决于初始种子-在许多情况下,它的最终输出接近均匀,而在很多时候,输出的明显不均匀。

这篇关于php rand()功能多么糟糕的启发,我使用QuickRandom和制作了一些随机矩阵图像System.Random。此运行显示了有时种子可能会产生不良影响(在这种情况下,倾向于使用较低的数字),而这种情况System.Random相当均匀。

QuickRandom

System.Random

甚至更糟

如果我们QuickRandomnew QuickRandom(0.01, 1.03)获得此图像时进行初始化:

代码

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

2
好的代码。是的,那很酷。我以前常常这样做,很难从中得出可量化的指标,但这是查看序列的另一种好方法。而且,如果您要查看长于宽*高的序列,则可以用每像素一个像素对下一张图像进行异或运算。我认为QuickRandom图片的质感就像是海藻地毯,在美学上却令人愉悦。
Cris Stringfellow

从美学上令人愉悦的部分是,当您沿着每一行(然后再回到起点)时,该序列如何趋于增加,因为magicNumber乘法产生的数字类似于prevNum,这表明缺乏随机性。如果我们使用种子,new QuickRandom(0.01, 1.03)则会得到i.imgur.com/Q1Yunbe.png
卡勒姆·罗杰斯

是的,很好的分析。因为它只是在包装发生之前将mod 1乘以一个常数,所以您所描述的会有所增加。如果我们乘以10亿乘以然后减小mod 256的调色板来获得不那么重要的小数位,似乎可以避免这种情况。
Cris Stringfellow

您能告诉我您用来生成这些输出图像的内容吗?Matlab的?
2013年

@uDaY:看一下C#和代码System.Drawing.Bitmap
卡勒姆·罗杰斯

37

您的随机数生成器的一个问题是没有“隐藏状态”-如果我知道您在上次通话中返回的随机数,那么我知道您将发送到时间结束的每个随机数,因为只有一个可能的下一个结果,依此类推。

要考虑的另一件事是随机数生成器的“周期”。显然,状态大小有限,等于double的尾数部分,因此在循环之前最多只能返回2 ^ 52的值。但这是最好的情况-您可以证明没有周期1,2,3,4 ...的循环吗?如果存在,那么在这种情况下,您的RNG会出现可怕的简陋行为。

另外,您的随机数生成对于所有起点是否具有统一的分布?如果不是这样,那么您的RNG将会有偏差-或更糟的是,取决于起始种子,其以不同的方式产生偏差。

如果您能回答所有这些问题,那就太好了。如果不能,那么您就会知道为什么大多数人不重新发明轮子,而是使用经过验证的随机数生成器;)

(顺便说一句好话是:最快的代码是无法运行的代码。您可以制作世界上最快的random(),但是如果它不是非常随机的话,那就不好了)


8
对于所有种子,此生成器上至少有一个琐碎的循环:0 -> 0。根据种子的不同,可能还有很多其他种子。(例如,对于3.0的种子,0.5 -> 0.50.25 -> 0.75 -> 0.250.2 -> 0.6 -> 0.8 -> 0.4 -> 0.2,等)
duskwuff -inactive-

36

开发PRNG时,我经常做的一项常见测试是:

  1. 将输出转换为char值
  2. 将chars值写入文件
  3. 压缩文件

这让我快速迭代了大约1至20兆字节序列的“足够好” PRNG的想法。与任何肉眼检查相比,它还提供了更好的自上而下的图像,因为任何带有半字状态的“足够好”的PRNG都可以很快超过您的眼睛看到循环点的能力。

如果我真的很挑剔,我可能会采用好的算法并对它们进行DIEHARD / NIST测试,以获取更多的见识,然后再回去进行一些调整。

与频率分析相反,压缩测试的优势在于,它很容易构造出良好的分布:只需输出一个包含长度为0-255的所有字符的256长度的块,然后执行100,000次即可。但是此序列的长度为256。

歪斜的分布,即使是很小的偏差,也应该通过压缩算法来拾取,特别是如果您给它足够的序列(例如1兆字节)使用。如果某些字符,双字母组或n-gram的出现频率更高,则压缩算法可以将此分布偏斜编码为有利于使用较短代码字频繁出现的代码,从而获得压缩的增量。

由于大多数压缩算法是快速的,并且不需要任何实现(因为操作系统只是在周围徘徊),因此压缩测试对于快速评估您可能正在开发的PRNG的通过/失败非常有用。

祝您实验顺利!

哦,我使用下面的代码小模块对上面的rng进行了此测试:

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

结果是:

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

如果输出文件根本无法压缩,我会认为PRNG很好。老实说,我认为您的PRNG效果不佳,如此简单的结构在20兆欧的Megs中仅占16%。但是我仍然认为这是失败的。


2
是否进行成像,几年前测试随机生成器时,我对zip的想法还是一样的。
亚里斯多斯

1
感谢@Alexandre C.和Aristos和aidan。我相信你。
Cris Stringfellow

33

您可以实现的最快的随机生成器是这样的:

在此处输入图片说明

XD开个玩笑,除了这里所说的一切,我还想指出一下,测试随机序列“是一项艰巨的任务” [1],并且有几种测试可以检查伪随机数的某些属性,您可以找到一个他们很多在这里: http //www.random.org/analysis/#2005

评估随机生成器“质量”的一种简单方法是旧的卡方检验。

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

引用[1]

χ²检验的目的是检查所产生的数字是否合理分布。如果我们生成小于r的N个正数,那么我们期望得到大约N / r每个值个数。但是-这是问题的本质-所有值的出现频率不应完全相同:这不是随机的!

我们只需计算每个值出现的频率的平方和,然后按期望的频率进行缩放,然后减去序列的大小即可。该数字“χ²统计量”可以用数学方式表示为

卡方公式

如果χ²统计量接近r,则数字是随机的;如果距离太远,那么他们就不会。可以更精确地定义“接近”和“远离”的概念:存在一些表,这些表准确地表明统计信息与随机序列的属性之间的关系。对于我们正在执行的简单测试,统计量应在2√r之内

使用此理论和以下代码:

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

我得到以下结果:

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

对于QuickRandom,它离r (在之外 r ± 2 * sqrt(r))很远

话虽如此,QuickRandom可能很快,但是(如另一个答案所述)不如随机数生成器好


[1] SEDGEWICK ROBERT,《C语言中的算法》,Addinson Wesley出版公司,1990年,第516至518页


9
xkcd +1,这是一个了不起的wobsite(哦,这是个很好的答案):P
tckmn 2013年

1
谢谢,是的xkcd机架!XD
higuaro

理论上很好,但执行效果很差:代码容易出现整数溢出。在Java中,所有int[]元素都初始化为零,因此不需要这部分。当您加倍练习时,将其转换为浮动是没有意义的。最后:调用方法名称random1和random2很有趣。
bestsss

@bestsss感谢您的观察!我直接从C代码进行了翻译,并没有对它==进行过多的关注。我进行了一些修改并更新了答案。我非常感谢任何其他建议
higuaro 2013年

14

我使用JavaScript 快速汇总了您的算法,以评估结果。它从0到99生成100,000个随机整数,并跟踪每个整数的实例。

我注意到的第一件事是,您获得高数字的可能性较小。当seed1高和seed2低时,您看到的最多。在某些情况下,我只有3个数字。

充其量,您的算法需要一些改进。


8

如果该Math.Random()函数调用操作系统来获取一天中的时间,则无法将其与函数进行比较。您的函数是PRNG,而该函数正在争取真正的随机数。苹果和橘子。

您的PRNG可能很快,但是它没有足够的状态信息来重复很长一段时间(并且其逻辑不够复杂,甚至无法利用这么多的状态信息来达到可能的周期)。

周期是PRNG开始重复之前的序列长度。一旦PRNG机器将状态转换为与某个过去状态相同的状态,就会发生这种情况。从那里开始,它将重复从该状态开始的过渡。PRNG的另一个问题可能是唯一序列数量少,以及特定重复序列的退化收敛。也可能有不希望的模式。例如,假设当数字以十进制打印时,PRNG看起来相当随机,但是检查二进制值会发现,第4位只是在每次调用时在0和1之间切换。糟糕!

看看Mersenne Twister和其他算法。有多种方法可以在周期长度和CPU周期之间取得平衡。一种基本方法(在梅森·扭曲器中使用)是在状态向量中循环。也就是说,在生成数字时,它不是基于整个状态,而是仅基于状态数组中的几个单词进行一些位运算。但是在每个步骤中,算法也会在数组中四处移动,一次对内容进行一点点加扰。


5
除您的第一段外,我大都同意。内置的随机调用(在类Unix系统上为/ dev / random)也是PRNG。我会把任何算法上会产生随机数的东西都称为PRNG,即使种子很难预测。有一些使用放射性衰变,大气噪声等的“真实”随机数发生器,但是这些发生器通常产生相对较少的比特/秒。
马特·克劳斯

在Linux机器上,/dev/random是从设备驱动程序而非PRNG获得的真正随机性的来源。当没有足够的位可用时,它将阻塞。姊妹设备/dev/urandom也不会阻塞,但是它仍然不完全是PRNG,因为它会在可用时用随机位进行更新。
卡兹(Kaz)

如果Math.Random()函数调用操作系统来获取一天中的时间 -这绝对是不正确的。(在我所知道的任何Java风格/版本中)
bestsss

@bestsss这是来自最初的问题:我记得读过Math.random使用System.nanoTime()的地方。您的知识可能值得在此处或在答案中添加。我有条件地使用if。:)
Kaz

哈兹,nanoTime()+ counter / hash都用作java.util.Randomoracle / OpenJDK 的默认种子。只有种子才能使用,这是标准LCG。实际上,OP生成器为种子取2个随机数,这是可以的-因此与没什么区别java.util.RandomSystem.currentTimeMillis()是JDK1.4-中的默认种子
bestsss 2013年

7

那里有很多伪随机数生成器。例如,Knuth的ranarrayMersenne twister,或寻找LFSR生成器。克努斯(Knuth)具有纪念意义的“符号算法”(Seminumerical Algorithm)对区域进行了分析,并提出了一些线性同余生成器(实现简单,速度快)。

但我建议您坚持使用java.util.RandomMath.random,它们会快速运行,并且至少可以偶尔使用(例如游戏等)。如果您对发行版抱有偏执(某些蒙特卡洛程序或遗传算法),请检查其实现(可在某处找到源),并使用来自操作系统或random.org的某个真正随机数作为种子。。如果某些对安全性至关重要的应用程序需要这样做,则您必须精打细算。而且在那种情况下,您不应该相信这里有一些带有缺失位的彩色正方形喷出,我现在就闭嘴。


7

除非Random从多个线程访问单个实例(因为Randomsynchronized),否则对于您想到的任何用例,随机数生成性能都不太可能成为问题。

但是,如果确实是这样,并且您需要快速提供大量随机数,则您的解决方案就太不可靠了。有时它会给出良好的结果,有时它会给出可怕的结果(基于初始设置)。

如果您想要与Random班级相同的数字,只是更快,那么您可以摆脱那里的同步:

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

}

我只是简单地获取了java.util.Random代码,并删除了同步,因此其性能是Oracle HotSpot JVM 7u9上原始性能的两倍。它仍然比您的慢QuickRandom,但它给出的结果要一致得多。确切地说,对于相同的seed值和单线程应用程序,它提供原始Random相同的伪随机数。


此代码基于java.util.RandomOpenJDK 7u中的最新版本,该版本已获得GNU GPL v2的许可。


10个月后编辑

我只是发现您甚至不必使用上面的代码来获取不同步的Random实例。JDK中也有一个!

看一下Java 7的ThreadLocalRandom类。其中的代码与我上面的代码几乎相同。该类只是本地线程隔离的Random版本,适合于快速生成随机数。我唯一想到的缺点是您无法seed手动设置它。

用法示例:

Random random = ThreadLocalRandom.current();

2
@Edit Hmm,我可以在不太懒的时候比较QR,Math.random和ThreadLocalRandom。:)这很有趣,谢谢!
tckmn

1.您可以通过删除掩码来获得更高的速度,因为最高的16位不会影响所使用的位。2.您可以使用这些位,节省一个减法并获得更好的生成器(更大的状态;产品的最高有效位分布得最均匀,但是需要一些评估)。3. Sun家伙只是由Knuth实现了一个古老的RNG并添加了同步。:(
maaartinus

3

“随机”不仅仅是获取数字...。您拥有的是伪随机

如果伪随机足以满足您的目的,那么可以肯定,它会更快(并且XOR + Bitshift会比您拥有的更快)

罗尔夫

编辑:

好吧,在此答案过于草率之后,让我回答一下为什么您的代码更快的真正原因:

从JavaDoc for Math.Random()

此方法已正确同步,以允许多个线程正确使用。但是,如果许多线程需要以很高的速率生成伪随机数,则可以减少每个线程拥有自己的伪随机数生成器的竞争。

这可能就是为什么您的代码更快的原因。


3
几乎所有不涉及硬件噪声发生器或不直接连接操作系统I / O内容的事物,都是伪随机的。真正的随机性不能仅由算法产生。您需要从某个地方发出噪音。(某些OS的RNG通过测量诸如如何/何时移动鼠标,键入东西等来获取输入。以微秒到纳秒为单位进行测量,这可能非常不可预测。)
cHao 2013年

@OliCharlesworth:的确,据我所知,唯一真正的随机值是使用大气噪声找到的。
Jeroen Vannevel 2013年

@我...笨拙地回答。Math.random是伪随机的,并且也是同步的
rolfl 2013年

@rolfl:同步可以很好地解释为什么Math.random()速度较慢。要么必须同步,要么Random每次都创建一个新的,而这两个都不是很吸引人的性能。如果我关心性能,我会自己创建new Random并使用它。:P
cHao

@JeroenVannevel的放射性衰变也是随机的。
RxS

3

java.util.Random没有太大区别,这是Knuth描述的基本LCG。但是,它具有两个主要优点/不同之处:

  • 线程安全-每次更新都是一个CAS,比简单的编写要贵得多,并且需要一个分支(即使完美预测了单线程)。取决于CPU,可能会有很大的不同。
  • 未公开的内部状态-这对于不重要的事情非常重要。您希望随机数不可预测。

下面是在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));
    }

如果删除AtomicLong和未公开的状态(即使用的所有位long),则将获得比双倍乘/模更高的性能。

最后一点:Math.random除了简单的测试之外,不应将其用于任何其他用途,它很容易引起争用,如果同时有几个线程同时调用它,性能也会下降。它的一个鲜为人知的历史特征是在Java中引入了CAS-击败了臭名昭著的基准测试(首先是IBM通过内部函数,然后Sun提出了“来自Java的CAS”)


0

这是我在游戏中使用的随机函数。它非常快,并且分布良好(足够)。

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

1
这不能为问题提供答案。要批评或要求作者澄清,请在其帖子下方发表评论。
John Willemse 2014年

我认为已经确定原始算法不够好?也许足够好的一个例子可以启发如何改进它?
Terje 2014年

是的,也许吧,但是它根本无法回答问题,也没有数据支持您的算法实际上“足够好”。通常,随机数算法和紧密相关的加密算法永远不会像以编程语言实现它们的专家那样出色。因此,如果您可以支持您的主张并详细说明为什么它比“问题”中的算法更好,那么您至少将回答所提出的问题。
John Willemse 2014年

好吧……以编程语言实现它们的专家旨在“完美”分发,而在游戏中,您永远不需要它。您需要速度和“足够好”的分配。这段代码提供了这一点。如果在这里不合适,我将删除答案,没问题。
Terje 2014年

关于多线程,您对局部变量的使用是无操作的,如不使用volatile,编译器可以随意消除(或引入)局部变量。
maaartinus
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.