JEP 356: Enhanced Pseudo-Random Number Generators | 增强的伪随机数生成器
摘要
为伪随机数生成器(PRNG)提供新的接口类型和实现,包括可跳转的 PRNG 和一类额外的可分割 PRNG 算法(LXM)。
目标
- 使在应用程序中互换使用各种 PRNG 算法变得更加容易。
- 通过提供 PRNG 对象的流来更好地支持基于流的编程。
- 消除现有 PRNG 类中的代码重复。
- 仔细保留
java.util.Random
类的现有行为。
非目标
本项目的目标不是提供众多其他 PRNG 算法的实现,而是提供一个能够容纳其他 PRNG 算法的框架。然而,我们已经添加了三种在其他编程语言环境中已广泛部署的常见算法。
成功指标
新的 LXM 算法的输出通过了现有的知名 TestU01 和 PractRand 测试套件。
Pierre L'Ecuyer 和 Richard Simard. TestU01: 随机数生成器的经验测试 C 库. ACM Transactions on Mathematical Software 33, 4 (2007 年 8 月), 文章 22. ISSN 0098-3500. http://doi.acm.org/10.1145/1268776.1268777
Richard Simard. TestU01 版本 1.2.3 (2009 年 8 月). http://www.iro.umontreal.ca/~simardr/testu01/tu01.html
Pierre L'Ecuyer 和 Richard Simard. TestU01: 随机数生成器的经验测试 ANSI C 软件库:用户指南,精简版. 蒙特利尔大学计算机科学与运筹学系,2013 年 5 月. http://www.iro.umontreal.ca/~simardr/testu01/guideshorttestu01.pdf
Chris Doty-Humphrey. PractRand 版本 0.90. 2014 年 7 月. http://pracrand.sourceforge.net [ 这不是拼写错误。软件名称是“PractRand”,但 SourceForge 项目名称是“pracrand”。]
可跳转和可跳跃的 PRNG 算法通过了验证某些操作可交换性的测试。
动机
我们专注于 Java 中伪随机数生成器(PRNG)领域的五个改进方向:
尽管遗留的 PRNG 类
Random
、ThreadLocalRandom
和SplittableRandom
都支持大致相同的方法集,但在应用程序中用其他算法替换其中任何一个都是困难的。例如,如果应用程序使用Random
类的实例,那么它必然会声明类型为Random
的变量,这些变量不能持有SplittableRandom
类的实例;要将应用程序更改为使用SplittableRandom
,则需要更改用于持有 PRNG 对象的每个变量(包括方法参数)的类型。唯一的例外是ThreadLocalRandom
是Random
的子类,纯粹是为了允许类型为Random
的变量持有ThreadLocalRandom
的实例,但ThreadLocalRandom
重写了Random
的几乎所有方法。接口可以很容易地解决这个问题。遗留的
Random
、ThreadLocalRandom
和SplittableRandom
类都支持nextDouble()
和nextBoolean()
等方法,以及生成流的方法如ints()
和longs()
,但它们具有完全独立且几乎是通过复制粘贴得到的相同实现。重构此代码将使维护变得更加容易,而且文档将使得第三方更容易创建也支持相同完整方法集的新 PRNG 类。2016 年的测试揭示了
SplittableRandom
类使用的算法中的两个新弱点。一方面,通过相对较小的修订可以避免这些弱点。另一方面,也发现了一类新的可分割 PRNG 算法(LXM),它们几乎同样快速,甚至更容易实现,并且似乎可以完全避免SplittableRandom
容易出现的三类弱点。能够从 PRNG 中获取 PRNG 对象的流,这使得使用流方法表达某些类型的代码变得更加容易。
文献中有许多 PRNG 算法是不可分割的,但它们是可跳跃的(也许还可以长跳,即除了普通跳跃外,还能进行非常长的跳跃),这是一种与分割截然不同的属性,但它同样支持 PRNG 对象的流。在过去,在 Java 中很难利用这一属性。可跳跃的 PRNG 算法的例子包括 Xoshiro256** 和 Xoroshiro128+。
- Xoshiro256** 和 Xoroshiro128+:http://xoshiro.di.unimi.it
描述
我们提供了一个新的接口 RandomGenerator
,它为所有现有和新的 PRNG 提供了一个统一的 API。RandomGenerators
提供了名为 ints
、longs
、doubles
、nextBoolean
、nextInt
、nextLong
、nextDouble
和 nextFloat
的方法,以及它们当前所有的参数变体。
我们提供了四个新的专门化 RandomGenerator
接口:
SplittableRandomGenerator
扩展了RandomGenerator
,并提供了名为split
和splits
的方法。可分割性允许用户从现有的 RandomGenerator 派生一个新的 RandomGenerator,该 RandomGenerator 通常会产生统计上独立的结果。JumpableRandomGenerator
扩展了RandomGenerator
,并提供了名为jump
和jumps
的方法。可跳跃性允许用户向前跳跃一定数量的抽取。LeapableRandomGenerator
扩展了RandomGenerator
,并提供了名为leap
和leaps
的方法。可长跳性允许用户向前跳跃大量的抽取。ArbitrarilyJumpableRandomGenerator
扩展了LeapableRandomGenerator
,并提供了jump
和jumps
的额外变体,允许指定任意的跳跃距离。
我们提供了一个新的类 RandomGeneratorFactory
,用于定位和构造 RandomGenerator
实现的实例。RandomGeneratorFactory
使用 ServiceLoader.Provider
API 来注册 RandomGenerator
实现。
我们对 Random
、ThreadLocalRandom
和 SplittableRandom
进行了重构,以便共享它们的大部分实现代码,并进一步使这些代码也可被其他算法重用。这一重构创建了底层的非公开抽象类 AbstractRandomGenerator
、AbstractSplittableRandomGenerator
、AbstractJumpableRandomGenerator
、AbstractLeapableRandomGenerator
和 AbstractArbitrarilyJumpableRandomGenerator
,每个类仅提供 nextInt()
、nextLong()
方法(以及相关的 split()
、jump()
、jump()
和 leap()
、或 jump(distance)
方法的实现)。重构后,Random
、ThreadLocalRandom
和 SplittableRandom
继承了 RandomGenerator
接口。请注意,由于 SecureRandom
是 Random
的子类,因此 SecureRandom
的所有实例也自动支持 RandomGenerator
接口,无需对 SecureRandom
类或其任何关联的实现引擎进行重新编码。
我们还添加了扩展 AbstractSplittableRandomGenerator
(因此实现 SplittableRandomGenerator
和 RandomGenerator
)的底层非公开类,以支持 LXM 家族 PRNG 算法的六个特定成员:
L32X64MixRandom
L32X64StarStarRandom
L64X128MixRandom
L64X128StarStarRandom
L64X256MixRandom
L64X1024MixRandom
L128X128MixRandom
L128X256MixRandom
L128X1024MixRandom
LXM 算法的核心 nextLong
(或 nextInt
)方法结构遵循了塞巴斯蒂亚诺·维格纳(Sebastiano Vigna)在 2017 年 12 月提出的建议,即使用一个线性同余生成器(LCG)子生成器和一个基于异或(xor)的子生成器(而不是两个 LCG 子生成器),可以提供更长的周期、更优的均匀分布、可扩展性和更好的质量。这里的每个具体实现都将目前已知的最佳基于异或的生成器之一(由布莱克曼(Blackman)和维格纳在《加扰线性伪随机数生成器》(ACM Trans. Math. Softw.,2021)中描述的 xoroshiro 或 xoshiro)与一个使用目前已知最佳乘数(由斯蒂尔(Steele)和维格纳在 2019 年通过搜索更好的乘数发现)的 LCG 相结合,然后应用道格·利(Doug Lea)确定的混合函数。测试已证实,LXM 算法的质量远优于 SplittableRandom
所使用的 SplitMix 算法(2014 年)。
我们还提供了这些广泛使用的伪随机数生成器(PRNG)算法的实现:
Xoshiro256PlusPlus
Xoroshiro128PlusPlus
上述提到的非公开抽象实现将来可能作为随机数实现者服务提供者接口(SPI)的一部分提供。
这套算法为 Java 程序员在空间、时间、质量和与其他语言的兼容性之间提供了合理的权衡范围。
替代方案
我们考虑过仅引入新接口,而不改变 Random
、ThreadLocalRandom
和 SplittableRandom
的实现。这有助于使 PRNG 对象更容易互换,但不会使它们的实现变得更容易。
我们还考虑过对 Random
、ThreadLocalRandom
和 SplittableRandom
进行重构,而不改变它们的功能或添加任何新接口。我们认为这可以减少它们的总体内存占用,但不会对未来 PRNG 算法的实现或使用提供任何便利。
测试
应继续使用针对
Random
、ThreadLocalRandom
和SplittableRandom
的所有现有测试。新测试(可能仅需执行一次):在修复新发现的两个弱点之前,应对重构后的
Random
、ThreadLocalRandom
和SplittableRandom
版本的输出进行抽查,以验证其行为是否保持不变,并与现有的(JDK 8)实现进行比较。新测试(可能仅需执行一次):应对 LXM 算法的输出进行抽查,并与用于使用 TestU01 和 PractRand 进行质量验证的 C 语言版本进行比较。
新测试(将成为测试套件的永久部分):应测试
jump()
和leap()
方法,以验证它们是否按声明的距离遍历状态周期。例如,从任何特定初始状态开始,操作序列nextLong(); jump()
应当使生成器处于与操作序列jump(); nextLong()
相同的状态。
风险和假设
我们认为这是一个中等规模的项目,风险很小。最大的负担可能是制定规范,其次是测试。
我们已小心谨慎地确保遗留随机数生成器的行为未受影响。