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

我开发了一个叫 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);
}

这是一个非常简单的算法,只需将前一个双精度数乘以一个“魔术数字”双精度数即可。我很快就把它拼凑起来了,所以我可能会做得更好,但奇怪的是,它似乎工作得很好。

这是 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()和大量疯狂的模数和除法的东西。有这个必要吗?我的算法运行得更快,而且看起来很随机。

我有两个问题:

  • 我的算法是否“足够好”(比如说,对于一个 真的随机数不太重要的游戏) ?
  • 为什么 Math.random做这么多,当它看起来只是简单的乘法和删除小数就足够了?
14297 次浏览

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

如果伪随机对于您的目的来说足够好,那么当然,它的速度要快得多(而且 XOR + B 它的移动将比您所拥有的更快)

劳夫

编辑:

好的,在回答这个问题时太草率了,让我来回答为什么你的代码更快的真正原因:

来自数学的 JavaDoc

此方法被正确同步以允许多个线程正确使用。然而,如果许多线程需要以很高的速度生成伪随机数生成器,那么每个线程拥有自己的伪随机数生成器可能会减少争用。

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

您所描述的是一种称为 线性同余方法的随机发生器。该发生器的工作原理如下:

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

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

希望这个能帮上忙!

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

0.10 -> 0.20

强烈地反映了类似的序列:

0.09 -> 0.18
0.11 -> 0.22

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

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

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

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

另外,你的随机数生成是否对所有的起点都有一个统一的分布?如果没有,那么您的 RNG 将是有偏见的-或更糟糕的是,偏见在不同的方式取决于开始的种子。

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

(顺便说一句,有句谚语说得好: 最快的代码是不运行的代码。您可以创建世界上最快的随机() ,但是如果不是非常随机的话就没有用了

我将 算法的快速模型放在 JavaScript 中以评估结果。它从0到99生成100,000个随机整数,并跟踪每个整数的实例。

我注意到的第一件事是,你更有可能得到一个较低的数字而不是一个较高的数字。当 seed1高而 seed2低时,这种情况最常见。在几个例子中,我只得到了3个号码。

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

您的 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  :

如果 Math.Random()函数调用操作系统来获取一天中的时间,那么您就不能将它与您的函数进行比较。您的函数是一个 PRNG,而该函数正在努力实现真正的随机数。苹果和橘子。

您的 PRNG 可能是快速的,但是它没有足够的状态信息来实现长时间的重复(并且它的逻辑不够复杂,甚至不能实现那么多状态信息所能实现的时间段)。

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

看看梅森旋转算法和其他算法。有很多方法可以在周期长度和 CPU 周期之间取得平衡。一个基本的方法(在梅森旋转算法中使用)是在状态向量中循环。也就是说,当生成一个数字时,它不是基于整个状态,而是基于状态数组中的几个字,只需要执行几个位操作。但是在每个步骤中,算法也在数组中移动,一次对内容进行一点点扰乱。

有很多很多的伪随机数生成器。例如 Knuth 的 Ranarray梅森旋转算法,或者寻找 LFSR 生成器。Knuth 的里程碑式的“半数算法”分析了该区域,并提出了一些线性同余生成器(简单实现,快速)。

但是我建议你还是坚持使用 java.util.Random或者 Math.random,它们很快,至少可以偶尔使用(比如游戏之类的)。如果你只是偏执的分布(一些蒙特卡罗程序,或遗传算法) ,检查他们的实现(来源是可用的地方) ,并种子他们与一些真正的随机数,无论是从您的操作系统或从 Org。如果某些安全性至关重要的应用程序需要这样做,那么您必须深入研究自己。如果是这样的话,你不会相信这里有什么彩色的正方形缺了点什么,我现在就闭嘴。

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

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

这使我能够快速地迭代那些“足够好”的 PRNG,其序列大约为1-20MB。它还提供了一个更好的自上而下的图片比只是检查它的眼睛,因为任何“足够好”的 PRNG 与半个词的状态可以迅速超过你的眼睛能力看到的周期点。

如果我真的很挑剔,我可能会选择好的算法,对它们进行 DIEHARD/NIST 测试,以获得更多的洞察力,然后再回去进行更多的调整。

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

一个倾斜的分布,即使是很小的边缘,也应该被压缩算法识别出来,特别是如果你给它足够(比如说1MB)的序列来处理的话。如果某些字符、双字符或 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 会做得这么好,只有16% 的约20兆赫是相当令人印象深刻的这样一个简单的建设。但我还是认为这是失败的。

Random 与 Knuth 描述的基本 LCG 没有太大的不同,但是它有两个主要的优点/区别:

  • 线程安全-每个更新都是一个 CAS,它比简单的写操作开销更大,并且需要一个分支(即使是完全预测的单线程)。根据 CPU 的不同,可能会有显著差异。
  • 未公开的内部状态-这对于任何重要的事情都是非常重要的。你希望随机数不是可预测的。

下面是 java.util.Random 中生成‘ 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 制作了“ CAS from Java”)

真正的问题在于它的输出直方图在很大程度上依赖于初始种子——大多数时候它会以接近均匀的输出结束,但是大多数时候会有明显不均匀的输出。

这篇文章介绍了 php 的 rand()函数有多糟糕的启发,我使用 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;
}
}
}

除非从多个线程访问单个 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类一样。


这个代码是基于当前的 OpenJDK 7u 中的 java.util.Random,它是根据 GNU GPL v2授权的。


10个月后:

我刚刚发现,您甚至不需要使用我上面的代码就可以得到一个非同步的 Random实例。JDK 里也有一个!

看看 Java7的 ThreadLocalRandom类。里面的代码和我上面的代码几乎一模一样。这个类只是一个本地线程隔离的 Random版本,适合快速生成随机数。我能想到的唯一缺点是无法手动设置它的 seed

示例用法:

Random random = ThreadLocalRandom.current();

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

enter image description here

XD,除了笑话,除了这里所说的一切,我想提供援引 测试随机序列“是一项艰巨的任务”[1] ,有几个测试 检查伪随机数的某些性质,你可以找到很多 这里: http://www.random.org/analysis/#2005

评估随机生成器“质量”的一个简单方法是旧的 X 平方分布检验。

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]

Χ2检验的思想是检查产生的数字是否是 如果我们产生的 N正数小于 R,那么我们就可以 期望得到每个值的大约 N/R数。但是——-这是 所有数值出现的频率不应该 一样的,那不是随机的!

我们简单地计算出发生频率的平方和 每个值,按照预期的频率进行缩放,然后减去 这个数字,“ χ2统计量”,可以用数学方法表示为

chi squared formula

如果 χ2统计量接近于 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]塞吉维克罗伯特,C 中的算法,阿丁森卫斯理出版公司,1990年,第516至518页

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

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