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);
}
_
これは、前のdoubleに「マジックナンバー」doubleを掛けるだけの非常に単純なアルゴリズムです。私はそれをかなり早くまとめたので、おそらくもっと良くすることができましたが、奇妙なことに、うまく機能しているようです。
これは、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()
とたくさんのクレイジーなモジュラスと除算を使用したことをどこかで読んだことを覚えています。それは本当に必要ですか?私のアルゴリズムは非常に高速に実行され、かなりランダムに見えます。
2つの質問があります。
Math.random
_はそんなに多くのことをするのでしょうか?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分布が安定していることがわかります。所望の均一な分布に達することもありますが、多くの場合そうではありません。極端な例の1つを次に示します。グラフの枠を超えています。
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 :
説明しているのは、 線形合同ジェネレーター と呼ばれるランダムジェネレーターのタイプです。ジェネレーターは次のように機能します。
このジェネレーターには多くのNiceプロパティがありますが、優れたランダムソースとして重大な問題があります。上にリンクされているウィキペディアの記事は、長所と短所のいくつかを説明しています。つまり、適切なランダム値が必要な場合、これはおそらくあまり良いアプローチではありません。
お役に立てれば!
内部状態が少なすぎるため、乱数関数は貧弱です。任意のステップで関数が出力する数値は、以前の数値に完全に依存しています。たとえば、magicNumber
が2(例として)であると仮定すると、シーケンスは次のようになります。
0.10 -> 0.20
同様のシーケンスによって強くミラーリングされます:
0.09 -> 0.18
0.11 -> 0.22
多くの場合、これによりゲーム内で顕著な相関関係が生成されます。たとえば、オブジェクトのX座標とY座標を生成するために関数を連続して呼び出すと、オブジェクトは明確な斜めパターンを形成します。
乱数ジェネレーターがアプリケーションの速度を低下させていると信じる正当な理由がない限り(そして、これは非常にありそうもないことですが)、独自に作成しようとする正当な理由はありません。
これの本当の問題は、出力ヒストグラムが初期シードに大きく依存していることです-ほとんどの場合、ほぼ均一な出力になりますが、多くの場合、明らかに不均一な出力になります。
このphpのRand()
関数の悪さに関する記事 に触発されて、QuickRandom
と_System.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;
}
}
}
_
乱数ジェネレーターの問題の1つは、「隠された状態」がないことです。最後の呼び出しで返された乱数を知っていれば、時間の終わりまで送信するすべての乱数を知っています。可能な次の結果など。
考慮すべきもう1つのことは、乱数ジェネレーターの「期間」です。明らかに、doubleの仮数部に等しい有限状態サイズでは、ループする前に最大2 ^ 52の値しか返せません。しかし、それは最良のケースです-期間1、2、3、4 ...のループがないことを証明できますか?存在する場合、RNGはこれらの場合にひどく退化した動作をします。
さらに、乱数生成はすべての開始点に対して均一な分布になりますか?そうでない場合は、RNGにバイアスがかかります。または、さらに悪いことに、開始シードに応じてさまざまな方法でバイアスがかけられます。
これらの質問のすべてに答えることができたら、素晴らしい。できない場合は、ほとんどの人が車輪を再発明して実績のある乱数ジェネレーターを使用しない理由を知っています;)
(ちなみに、良い格言は次のとおりです:最速のコードは実行されないコードです。世界で最速のrandom()を作成することはできますが、あまりランダムでないとダメです)
PRNGを開発するときに私がいつも行った1つの一般的なテストは、
これにより、約1〜20メガバイトのシーケンスに対して「十分に良い」PRNGであるアイデアをすばやく繰り返すことができました。また、「十分に良い」PRNG半ワードの状態でサイクルポイントを見る目の能力をすぐに超える可能性があるため、目で検査するよりも優れたトップダウン画像を提供しました。
私が本当にうるさいなら、より良い洞察を得るために、良いアルゴリズムを取り、DIEHARD/NISTテストを実行して、戻ってさらに調整するかもしれません。
頻度分析とは対照的に、圧縮テストの利点は、0〜255の値のすべての文字を含む256の長さのブロックを出力し、これを100,000回行うだけで、簡単に適切な分布を簡単に構築できることです。ただし、このシーケンスの長さは256です。
わずかなマージンでさえ、歪んだ分布は、特に十分な数(たとえば1メガバイト)のシーケンスを処理する場合に、圧縮アルゴリズムによって選択する必要があります。一部の文字、バイグラム、またはn-gramがより頻繁に発生する場合、圧縮アルゴリズムは、この分散スキューを、短いコードワードで頻繁に発生するコードにエンコードし、圧縮のデルタを取得できます。
ほとんどの圧縮アルゴリズムは高速であり、実装を必要としないため(OSには横になっているため)、圧縮テストは、PRNG開発中です。
実験を頑張ってください!
ああ、私はあなたのコードの次の小さなMODを使用して、上記の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%しかこのような単純な構造では印象的ではありませんが、それでも失敗だと思います。
実装できる最速のランダムジェネレーターは次のとおりです。
XD、冗談は、ここで述べたすべてに加えて、ランダムシーケンスのテストは「難しいタスク」[1]であり、擬似乱数の特定のプロパティをチェックするテストがいくつかあることを挙げて貢献したいと思います。それらの多くはここにあります: http://www.random.org/analysis/#2005
ランダムジェネレーターの「品質」を評価する1つの簡単な方法は、古いカイ二乗検定です。
_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各値の番号。しかし---そしてこれが問題の本質です---すべての値の出現頻度は正確に同じであってはなりません:それはランダムではないでしょう!
各値の出現頻度の二乗和を単純に計算し、予想される頻度でスケーリングしてから、シーケンスのサイズを差し引きます。この数、「χ²統計」は、数学的に次のように表現できます。
Χ²統計が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 Publishing Company、1990、516ページから518
JavaScriptで アルゴリズムの簡単なモックアップ をまとめて、結果を評価します。 0から99までの100,000個のランダムな整数を生成し、各整数のインスタンスを追跡します。
私が最初に気づくのは、あなたが高い数字よりも低い数字を得る可能性が高いということです。 seed1
は高く、seed2
低い。いくつかの例では、3つの数字しか得られませんでした。
せいぜい、あなたのアルゴリズムはいくらかの洗練を必要とします。
Math.Random()
関数が時刻を取得するためにオペレーティングシステムを呼び出す場合、それを関数と比較することはできません。あなたの関数はPRNGですが、その関数は実際の乱数を求めています。リンゴとオレンジ。
あなたのPRNGは速いかもしれませんが、繰り返す前に長い期間を達成するのに十分な状態情報を持っていません(そしてそのロジックは、それだけで可能な期間を達成するほど十分に洗練されていません状態情報)。
期間は、PRNGが繰り返される前のシーケンスの長さです。これは、PRNGマシンが状態に遷移すると同時に発生します。 PRNGのもう1つの問題は、固有のシーケンスの数が少ないことと、繰り返される特定のシーケンスの縮退収束である可能性があります。たとえば、数値が10進数で出力される場合、a PRNGはかなりランダムに見えますが、バイナリの値を調べると、ビット4がそれぞれの0と1の間で単純にトグルしていることがわかりますお電話ください。
Mersenne Twisterおよびその他のアルゴリズムを見てください。期間の長さとCPUサイクルのバランスをとる方法があります。 1つの基本的なアプローチ(Mersenne Twisterで使用)は、状態ベクトル内を循環することです。つまり、数が生成されているとき、それは状態全体に基づいているのではなく、数ビット操作の対象となる状態配列からの数ワードに基づいています。しかし、各ステップで、アルゴリズムは配列内を動き回って、少しずつ内容をスクランブルします。
そこには、非常に多くの疑似乱数ジェネレータがあります。たとえば、Knuthの ranarray 、 Mersenneツイスター 、またはLFSRジェネレーターの検索。 Knuthの記念碑的な「半数論的アルゴリズム」は、領域を分析し、いくつかの線形合同ジェネレーターを提案します(実装が簡単、高速)。
ただし、Java.util.Random
またはMath.random
、彼らは高速で、少なくとも時折の使用(つまり、ゲームなど)には問題ありません。ディストリビューション(一部のモンテカルロプログラム、または遺伝的アルゴリズム)に執着している場合は、その実装を確認し(ソースはどこかで入手可能)、オペレーティングシステムまたは random.org 。セキュリティが重要なアプリケーションでこれが必要な場合は、自分で掘る必要があります。そして、その場合、ここに欠けているビットのある色付きの正方形がここで噴出するのを信じてはいけないので、私は今黙ります。
乱数生成のパフォーマンスが、複数のスレッドから単一のRandom
インスタンスにアクセスしない限り、どのようなユースケースでも問題になることはほとんどありません(Random
はsynchronized
であるため)。
ただし、 really が事実であり、多くの乱数を高速で必要とする場合、ソリューションの信頼性は非常に低くなります。良い結果が得られることもあれば、 horrible 結果(初期設定に基づく)が得られることもあります。
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
コードを削除し、同期を削除したため、 twice がOracle HotSpot JVM 7u9の元のパフォーマンスと比較されました。それでもQuickRandom
より遅いですが、より一貫した結果が得られます。正確には、同じseed
値とシングルスレッドアプリケーションの場合、元のRandom
クラスと同じ同じ擬似乱数を与えます。
このコードは、現在の Java.util.Random
OpenJDK 7 = GNU GPL v2 でライセンスされています。
[〜#〜] edit [〜#〜]10か月後:
同期されていないRandom
インスタンスを取得するために上記のコードを使用する必要さえないことを発見しました。 JDKにも1つあります!
Java 7の ThreadLocalRandom
クラスを見てください。その中のコードは上記の私のコードとほとんど同じです。このクラスは、生成に適したローカルスレッド分離のRandom
バージョンです。考えられる唯一の欠点は、seed
を手動で設定できないことです。
使用例:
Random random = ThreadLocalRandom.current();
「ランダム」とは、単に数字を取得するだけではありません.... pseudo-random
擬似ランダムが目的に十分であれば、それはかなり高速です(そしてXOR + Bitshiftはあなたが持っているものよりも高速になります)
ロルフ
編集:
さて、この回答であまりにも急いだ後、あなたのコードが高速である本当の理由に答えさせてください:
Math.Random()のJavaDocから
このメソッドは、複数のスレッドで正しく使用できるように適切に同期されます。ただし、多数のスレッドが高速で擬似乱数を生成する必要がある場合、各スレッドが独自の擬似乱数ジェネレータを使用するための競合を減らすことができます。
これがおそらくコードが高速になる理由です。
Java.util.Randomはそれほど大きな違いはありません。Knuthが説明した基本的なLCGです。ただし、主な2つの利点/違いがあります。
以下は、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
は単純なテスト以外には使用すべきではありません。競合が発生しやすく、同時に複数のスレッドを呼び出す場合でもパフォーマンスが低下します。少し知られている歴史的特徴の1つは、CASの導入Java-悪名高いベンチマークに勝つために(最初にIBMが組み込み関数を使用して、次にSunが「CAS from Java」を作成した)
これは、ゲームに使用するランダム関数です。それは非常に高速で、良好な(十分な)配布を持っています。
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));
}
}