Javaの乱数生成器について速度面から調査してみる

Java17にjava.util.randomパッケージが追加されたので改めて乱数生成器について調査してみることにしました。

ちなみに本記事では特にどの乱数生成器がよいというような検証はしません。（用途によって向き不向きがあるはずなので）

# ベンチマーク環境

* OS
    * Windows10Pro
        * WSL1
            * Ubuntu20.04LTS
* CPU
    * Core i7-10700 (8core 16thread)
* Memory
    * 32GB
* Java
    * OpenJDK17
* Apache Maven
    * 3.8.3

# コード

https://github.com/penguinsan/random_bench

# 調査したアルゴリズムについて

各アルゴリズムについてシングルスレッドの場合とマルチスレッドの場合でテストしています。

マルチスレッドは4スレッドとし、実行したPCのCPUコア数内に収めています。

※Java16までを使う場合の参考としてApache Commons RNGの各アルゴリズムのテストも入っていますが、本記事では割愛。

※参考用にjava.util.Randomを含めているものの、品質が悪いため実環境では使用しません。

* シングルスレッド
    1. java.util.Random
    1. java.security.SecureRandom (NativePRNGNonBlocking)
    1. java.util.concurrent.ThreadLocalRandom
    1. java.util.random.RandomGenerator (L64X128MixRandom)
    1. java.util.random.RandomGenerator (Xoroshiro128PlusPlus)
* マルチスレッド
    1. java.util.Random
    1. java.security.SecureRandom (NativePRNGNonBlocking)
    1. java.util.concurrent.ThreadLocalRandom
    1. java.util.random.RandomGenerator (L64X128MixRandom) ThreadLocal
    1. java.util.random.RandomGenerator (Xoroshiro128PlusPlus) ThreadLocal

マルチスレッドで後ろにThreadLocalとついているものはRandomGeneratorがスレッドセーフではないため、スレッド毎にインスタンスを作成するようにしています。

# 結果

## シングルスレッド

```
Benchmark                                                                    Mode  Cnt           Score          Error  Units
RandomBenchmarkSingleThread.jdkRandom(1)                                    thrpt    5   131639688.221 ±   832868.840  ops/s
RandomBenchmarkSingleThread.secureRandom(2)                                 thrpt    5     2656059.305 ±    14842.602  ops/s
RandomBenchmarkSingleThread.threadLocalRandom(3)                            thrpt    5   501922756.671 ±  2784109.670  ops/s
RandomBenchmarkSingleThread.jdk17L64X128MixRandom(4)                        thrpt    5   331029209.781 ±  3377450.451  ops/s
RandomBenchmarkSingleThread.jdk17Xoroshiro128ppRandom(5)                    thrpt    5   439135306.643 ±  4206646.624  ops/s
```

## マルチスレッド
```
Benchmark                                                                    Mode  Cnt           Score          Error  Units
RandomBenchmarkMultiThread.jdkRandom(1)                                     thrpt    5    15938216.638 ±  1536272.415  ops/s
RandomBenchmarkMultiThread.secureRandom(2)                                  thrpt    5     1835105.725 ±    10398.678  ops/s
RandomBenchmarkMultiThread.threadLocalRandom(3)                             thrpt    5  1915947749.858 ± 96167030.714  ops/s
RandomBenchmarkMultiThread.threadLocalJdk17L64X128MixRandom(4)              thrpt    5   714011780.505 ± 25031734.867  ops/s
RandomBenchmarkMultiThread.threadLocalJdk17Xoroshiro128ppRandom(5)          thrpt    5  1035939838.978 ± 77871089.459  ops/s
```

# 考察

RandomやSecureRandomはスレッドセーフではあるものの内部で同期化されているのでマルチスレッドで性能が悪化しています。

また、Java17で追加された乱数生成器はスレッドセーフではないためThreadLocalでラップして実験しましたが、4スレッド時に2倍強程度の性能なのでそこまで性能が伸びませんでした。（マルチスレッドで性能が伸びないのはThreadLocalの性能のせいかもしれません）

性能面では以前から存在しているThreadLocalRandomが最も高速でかつマルチスレッド時にほぼスレッド数に比例した性能が出ていることがわかります。

個人的には、暗号やセッションキーの生成等ではSecureRandom。それ以外は通常ThreadLocalRandom。長い周期が必要な場合(ThreadLocalRandomは2の64乗周期でそれほど長くない)に適宜アルゴリズムを選んで使うといったところでしょうか。

ちなみにJava17で利用できるアルゴリズムや周期等の情報は[Javadocのjava.util.randomパッケージ](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/package-summary.html#algorithms)に載っています。