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

时间:2013-01-24 00:35:43

标签: java performance algorithm random

我创建了一个名为QuickRandom的类,它的工作是快速生成随机数。它非常简单:只需取旧值,乘以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);
}

这是一个非常简单的算法,只需将前一个双倍乘以&#34;幻数&#34;双。我把它快速地扔到了一起,所以我可能会把它变得更好,但奇怪的是,它看起来工作得很好。

这是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

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

我记得在Math.random使用System.nanoTime()以及大量疯狂模数和分数的地方读书。这真的有必要吗?我的算法执行速度更快,看起来很随机。

我有两个问题:

  • 我的算法是否足够好&#34; (比方说,一个游戏,其中真的随机数并不重要)?
  • 为什么Math.random看起来只是简单的乘法并且删除小数就足够了?

14 个答案:

答案 0 :(得分: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  :                                                  

答案 1 :(得分:133)

您所描述的是一种称为linear congruential generator的随机生成器。生成器的工作原理如下:

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

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

希望这有帮助!

答案 2 :(得分:112)

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

0.10 -> 0.20

强烈反映了相似的序列:

0.09 -> 0.18
0.11 -> 0.22

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

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

答案 3 :(得分:108)

这个问题的真正问题在于它的输出直方图在很大程度上取决于初始种子 - 大部分时间它会以接近均匀的输出结束,但很多时候会有明显不均匀的输出。

this article about how bad php's rand() function is的启发,我使用QuickRandomSystem.Random制作了一些随机矩阵图像。此次运行显示了种子有时会产生不良影响(在这种情况下偏向较低的数字),而System.Random非常均匀。

QuickRandom

System.Random

甚至更糟糕

如果我们将QuickRandom初始化为new 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;
        }
    }
}

答案 4 :(得分:37)

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

要考虑的另一件事是随机数生成器的“周期”。显然,对于有限状态大小,等于double的尾数部分,它只能在循环之前返回最多2 ^ 52个值。但这是最好的情况 - 你能证明没有第1,2,3,4期的循环......?如果有,那么你的RNG在这些情况下会有可怕的退化行为。

此外,您的随机数生成是否会为所有起点分布均匀?如果没有,那么你的RNG将会有偏见 - 或者更糟糕的是,取决于起始种子,会有不同的偏见。

如果你能回答所有这些问题,真棒。如果你做不到,那你就知道为什么大多数人不重新发明轮子并使用经过验证的随机数发生器;)

(顺便说一下,一个好的格言是:最快的代码是不运行的代码。你可以在世界上做出最快的随机(),但如果它不是非常随机则没有好处)< / em>的

答案 5 :(得分:36)

我在开发PRNG时经常做的一个常见测试是:

  1. 将输出转换为字符值
  2. 将字符值写入文件
  3. 压缩文件
  4. 这让我可以快速迭代对于大约1到20兆字节序列的“足够好”PRNG的想法。它还提供了一个更好的自上而下的图片而不仅仅是通过眼睛检查它,因为任何“足够好”的PRNG具有半字状态可能很快超过你的眼睛看周期点的能力。

    如果我真的很挑剔,我可能会采用好算法并对它们进行DIEHARD / NIST测试,以获得更多洞察力,然后再回过头来调整一些。

    与频率分析相比,压缩测试的优点在于,通常很容易构建良好的分布:只需输出一个256长度的块,其中包含0到255之间的所有字符,并执行此操作100,000次。但是这个序列的长度为256.

    偏差分布,即使是很小的余量,也应该通过压缩算法来获取,特别是如果你给它足够的(比如1兆字节)序列。如果更频繁地出现某些字符,双字节或n字符,则压缩算法可以将此分布偏差编码为有利于频繁出现的代码,并且您获得压缩增量。

    由于大多数压缩算法都很快,并且它们不需要实现(因为操作系统只是在它们周围),压缩测试对于快速评估您可能正在开发的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不会那么好,对于这样一个简单的结构,只有16%的~20 Megs相当令人印象深刻。但我仍然认为它失败了。

答案 6 :(得分:33)

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

enter image description here

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]

  

χ²检验的想法是检查产生的数字是否是   合理地分散。如果我们生成 N 正数小于 r ,那么我们就会   期望获得每个值的 N / r 数字。但是---这就是本质   问题---所有值的发生频率不应该是精确的   同样的:那不会是随意的!

     

我们简单地计算出现的频率的平方和   每个值,按预期频率缩放,然后减去大小   序列。这个数字,即“χ²统计量”,可以用数学表示为

chi squared formula

  

如果χ²统计量接近 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,Algorithms in C,Addinson Wesley Publishing Company,1990,page 516 to 518

答案 7 :(得分:14)

我将a quick mock-up of your algorithm放在JavaScript中以评估结果。它从0到99生成100,000个随机整数,并跟踪每个整数的实例。

我注意到的第一件事是你更有可能得到一个低数字而不是一个高数字。当seed1为高且seed2为低时,您会发现此最多。在一些情况下,我只得到3个数字。

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

答案 8 :(得分:8)

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

你的PRNG速度可能很快,但是它没有足够的状态信息可以在重复之前实现很长一段时间(并且它的逻辑不够复杂,甚至无法实现那么多状态信息可能达到的时间段)。

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

看看Mersenne Twister和其他算法。有一些方法可以在周期长度和CPU周期之间取得平衡。一种基本方法(在Mersenne Twister中使用)是在状态向量中循环。也就是说,当正在生成数字时,它不是基于整个状态,而是基于来自状态数组的几个字经受几位操作。但是在每一步中,算法也会在数组中移动,一次一点地加扰内容。

答案 9 :(得分:7)

那里有很多很多伪随机数发生器。例如Knuth的ranarrayMersenne twister,或寻找LFSR生成器。 Knuth的巨大“Seminumerical算法”分析了该区域,并提出了一些线性同余生成器(易于实现,快速)。

但我建议你坚持java.util.RandomMath.random,他们禁食,至少可以偶尔使用(比如游戏等)。如果您对分布(一些蒙特卡罗程序或遗传算法)只是偏执,请查看它们的实现(源代码可以在某处获得),并使用您的操作系统或{{3 }}。如果某些安全性至关重要的应用程序需要这样做,那么您必须自己挖掘。而且在那种情况下你不应该相信这里有一些缺少比特的彩色方块,我现在会闭嘴。

答案 10 :(得分: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.Random in OpenJDK 7u下获得许可的当前GNU GPL v2


编辑 10个月后:

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

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

使用示例:

Random random = ThreadLocalRandom.current();

答案 11 :(得分:3)

'随机'不仅仅是获取数字......你所拥有的是pseudo-random

如果伪随机对你的目的足够好,那么肯定,它会更快(并且XOR + Bitshift将比你的更快)

罗尔夫

编辑:

好的,在这个答案过于仓促之后,让我回答你的代码更快的真正原因:

来自JavaDoc for Math.Random()

  

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

这可能是您的代码更快的原因。

答案 12 :(得分: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”)

答案 13 :(得分: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));
       }
}