javaの乱数についてメモ。
java.util.Randomクラス
おそらくもっともサンプルで出てくる初期(jdk1.0)から存在するクラス。
Math.random()はこのRandom.nextDouble()を呼んでいる。
線形合同式のため、過去の一覧から次の乱数が予測できるらしい。
スレッドセーフなのは、AtomicLongを使っている。
この式はクヌース先生のThe Art of Computer Programmingから引用って、1968年出版ですか。
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));
}
自作系Xorshiftクラス
Wikipedia Xorshiftにある。
線形合同式よりは新しい2003年頃。
シードは1以外
xorshift32
double nextDouble() {
nextLong();
return (double) ((long) state & 0xffffffffL) * (1.0 / (double) (0xffffffffL));
}
int nextLong() {
state ^= state << 13;
state ^= state >>> 17;
state ^= state << 5;
return state;
}
xorshift64
double nextDouble() {
nextLong();
return (double) (state & Long.MAX_VALUE) * (1.0 / (double) (Long.MAX_VALUE));
}
long nextLong() {
state ^= state << 13;
state ^= state >>> 7;
state ^= state << 17;
return state;
}
java.util.concurrent.ThreadLocalRandomクラス
jdk1.7で追加。
スレッドローカルを使っているから、Randomクラスをスレッドローカルで管理しているのかと思えば、まったく違う式だった。
Unsafe U = Unsafe.getUnsafe();って、もうjavaじゃない部分。
単にGOLDEN_GAMMAを足しているだけだが、スレッドIDも含まれている。
したがって、同じシードでも、スレッドIDが異なれば、異なる乱数パターンとなるが、毎回再現したくてシードを同じにしているのに、スレッドIDが異なったら再現できないのは使いにくいのではないか。
ThreadLocalRandomクラスにsetSeedは公開されているが、実際に呼び出すとUnsupportedOperationExceptionとなる。
final long nextSeed() {
Thread t; long r; // read and update per-thread seed
U.putLong(t = Thread.currentThread(), SEED,
r = U.getLong(t, SEED) + (t.threadId() << 1) + GOLDEN_GAMMA);
return r;
}
java.util.SplittableRandomクラス
jdk1.8で追加。
独立した並列計算に使用可能だが、スレッドセーフではないと、ややこしい。
乱数ジェネレータの独立性と一様性をテストするDieHarderスイートに合格します。と書いてあるので、乱数としてはマシな感じ。(RandomはDieHarderテストの一部に通っていないとどこかに書いてあった)
ThreadLocalRandomクラスと同じ値GOLDEN_GAMMAを加えた後に、Xorshiftのアイデアも入っているような雰囲気。
setSeedは存在しないが、コンストラクタでseedは指定できる。
private long nextSeed() {
return seed += gamma;
}
private static long mix64(long z) {
z = (z ^ (z >>> 30)) * 0xbf58476d1ce4e5b9L;
z = (z ^ (z >>> 27)) * 0x94d049bb133111ebL;
return z ^ (z >>> 31);
}
private static int mix32(long z) {
z = (z ^ (z >>> 33)) * 0x62a9d9ed799705f5L;
return (int)(((z ^ (z >>> 28)) * 0xcb24d0a5c88c35b3L) >>> 32);
}
java.security.SecureRandomクラス
jdk1.2(1.4)から存在する。
暗号用の乱数。
中身はプロバイダーがプラグイン化されているので、おそらくネイティブコールされている。
SecureRandom数値生成アルゴリズムは、jdk1.8からjdk24でも変わっていない。
ただしWindowsで実行すると以下のものしか使えない。
- DRBG:NIST SP 800-90Ar1で定義されているDRBGメカニズムを使用するアルゴリズム。
- SHA1PRNG:Sunプロバイダが提供する擬似乱数生成(PRNG)アルゴリズム。 このアルゴリズムは、PRNGの基盤としてSHA-1を使用します。 各操作につき値が1増加する64ビット・カウンタを使って鎖状につながった真にランダムなシード値から、SHA-1ハッシュを計算します。 160ビットのSHA-1出力のうち、64ビットだけが使用されます。
- Windows-PRNG:基盤となるWindows OSから乱数を取得します。
その他リストにあるのは以下のもの。
- NativePRNG:基盤となるネイティブOSから乱数を取得します。 乱数生成のブロック性については何も表明されません。
- NativePRNGBlocking: 基盤となるネイティブOSから乱数を取得し、必要に応じてブロック化します。 たとえば、UNIX系システムの/dev/randomなど。
- NativePRNGNonBlocking:基盤となるネイティブOSから乱数を取得しますが、アプリケーションの速度低下を避けるためブロック化しません。 たとえば、UNIX系システムの/dev/urandomなど。
- PKCS11:基礎となるインストール済および構成済のPKCS #11ライブラリから乱数を取得します。
java.util.random.RandomGeneratorクラス
jdk17で追加。
ThreadLocalRandomは特別な使い方だが、それ以外はRandomGeneratorFactory.ofで共通に使える。
ただしseedの設定はできない。
LXMグループ内のアルゴリズムは互いに似ています。 各アルゴリズムのパラメータは、アルゴリズム名にあります。 "L"の後の数字はLCGサブ・ジェネレータの状態ビットの数を示し、"X"の後の数字はXBGサブ・ジェネレータの状態ビットの数を示します。 "ミックス"は、アルゴリズムで8操作ビット混合関数が使用されていることを示します。"StarStar"は、3操作ビット・スクラムブラの使用を示します。
- L32X64MixRandom:$2^{64}-2^{32}$
- L64X128MixRandom:$2^{128}-2^{64}$
- L64X128StarStarRandom:$2^{128}-2^{64}$
- L64X256MixRandom:$2^{256}-2^{64}$
- L64X1024MixRandom:$2^{1024}-2^{64}$
- L128X128MixRandom:$2^{128}-2^{128}$
- L128X256MixRandom:$2^{256}-2^{128}$
- L128X1024MixRandom:$2^{1024}-2^{128}$
- Xoroshiro128PlusPlus:$2^{128}-1$1
- Xoshiro256PlusPlus:$2^{256}-1$2
- Random:$2^{48}$
- SplittableRandom:$2^{64}$
- SecureRandom:Hash_DRBG,SHA-256,128,reseed_only3
Apache Commons RNG
cppのmtに相当するMersenneTwister64なるものが、入っている。
ファクトリーから使うには、以下の外部jarファイルが必要となる。
- commons-rng-client-api-1.6.jar
- commons-rng-core-1.6.jar
- commons-rng-simple-1.6.jar
RandomSourceにはいっぱいあるが、有りすぎるのでこれだけ実行してみる。
UniformRandomProvider mt = RandomSource.MT_64.create(1);
//MersenneTwister64 mt = new MersenneTwister64(new long[] { 1L });
for (int i = 0; i < max; i++) {
double v = mt.nextDouble();
}
実行時間の比較
すべてnextDouble相当を呼び出し。
数値の単位はミリ秒。
RandomGeneratorFactory,ThreadLocalRandom,Xorshift32,Xorshift64
| name | 100万 | 1000万 | 1億 |
|---|---|---|---|
| Random | 19 | 166 | 1565 |
| Xorshift32 | 5 | 17 | 137 |
| Xorshift64 | 5 | 20 | 161 |
| ThreadLocalRandom | 6 | 15 | 89 |
| SplittableRandom | 8 | 63 | 78 |
| L32X64MixRandom | 16 | 81 | 836 |
| L64X128MixRandom | 8 | 67 | 511 |
| L64X128StarStarRandom | 8 | 65 | 621 |
| L64X256MixRandom | 8 | 72 | 705 |
| L64X1024MixRandom | 9 | 80 | 794 |
| L128X128MixRandom | 11 | 103 | 808 |
| L128X256MixRandom | 11 | 86 | 998 |
| L128X1024MixRandom | 13 | 91 | 1102 |
| Xoroshiro128PlusPlus | 6 | 58 | 597 |
| Xoshiro256PlusPlus | 7 | 63 | 642 |
| SecureRandom | 1688 | 15238 | 158511 |
Randomが一番ショボいのに、とにかく遅い。あんな単純な式なのに、AtomicLongが足を引っ張っているような。
AIチャットは、L64X128MixRandomやXoroshiro128PlusPlusが、実行速度やメモリ使用量のバランスがよいとのこと。
ただし、自由にseedが設定できそうなのは、Random,Xorshift32,Xorshift64,SplittableRandom,SecureRandomくらい。
SecureRandom
| name | 100万 | 1000万 |
|---|---|---|
| DRBG | 1521 | 15264 |
| SHA1PRNG | 205 | 1531 |
| Windows-PRNG | 365 | 3458 |
こちらの用途から100万回も呼ぶようなものではないような。
RandomGeneratorFactoryで終わらないと思ったら、SecureRandomだった。
Apache Commons RNG
| name | 100万 | 1000万 | 1億 |
|---|---|---|---|
| RandomSource.MT_64 | 46 | 71 | 236 |
| new MersenneTwister64 | 37 | 52 | 227 |
初期化に多少時間がかかる。
100万、1000万と連続して実行すると、35-22-204のように、1000万件の方が速くなる。
ソース置き場