Skip to content

JEP 356: Enhanced Pseudo-Random Number Generators | 增强的伪随机数生成器

摘要

为伪随机数生成器(PRNG)提供新的接口类型和实现,包括可跳转的 PRNG 和一类额外的可分割 PRNG 算法(LXM)。

目标

  • 使在应用程序中互换使用各种 PRNG 算法变得更加容易。
  • 通过提供 PRNG 对象的流来更好地支持基于流的编程。
  • 消除现有 PRNG 类中的代码重复。
  • 仔细保留 java.util.Random 类的现有行为。

非目标

本项目的目标不是提供众多其他 PRNG 算法的实现,而是提供一个能够容纳其他 PRNG 算法的框架。然而,我们已经添加了三种在其他编程语言环境中已广泛部署的常见算法。

成功指标

新的 LXM 算法的输出通过了现有的知名 TestU01 和 PractRand 测试套件。

可跳转和可跳跃的 PRNG 算法通过了验证某些操作可交换性的测试。

动机

我们专注于 Java 中伪随机数生成器(PRNG)领域的五个改进方向:

  • 尽管遗留的 PRNG 类 RandomThreadLocalRandomSplittableRandom 都支持大致相同的方法集,但在应用程序中用其他算法替换其中任何一个都是困难的。例如,如果应用程序使用 Random 类的实例,那么它必然会声明类型为 Random 的变量,这些变量不能持有 SplittableRandom 类的实例;要将应用程序更改为使用 SplittableRandom,则需要更改用于持有 PRNG 对象的每个变量(包括方法参数)的类型。唯一的例外是 ThreadLocalRandomRandom 的子类,纯粹是为了允许类型为 Random 的变量持有 ThreadLocalRandom 的实例,但 ThreadLocalRandom 重写了 Random 的几乎所有方法。接口可以很容易地解决这个问题。

  • 遗留的 RandomThreadLocalRandomSplittableRandom 类都支持 nextDouble()nextBoolean() 等方法,以及生成流的方法如 ints()longs(),但它们具有完全独立且几乎是通过复制粘贴得到的相同实现。重构此代码将使维护变得更加容易,而且文档将使得第三方更容易创建也支持相同完整方法集的新 PRNG 类。

  • 2016 年的测试揭示了 SplittableRandom 类使用的算法中的两个新弱点。一方面,通过相对较小的修订可以避免这些弱点。另一方面,也发现了一类新的可分割 PRNG 算法(LXM),它们几乎同样快速,甚至更容易实现,并且似乎可以完全避免 SplittableRandom 容易出现的三类弱点。

  • 能够从 PRNG 中获取 PRNG 对象的流,这使得使用流方法表达某些类型的代码变得更加容易。

  • 文献中有许多 PRNG 算法是不可分割的,但它们是可跳跃的(也许还可以长跳,即除了普通跳跃外,还能进行非常长的跳跃),这是一种与分割截然不同的属性,但它同样支持 PRNG 对象的流。在过去,在 Java 中很难利用这一属性。可跳跃的 PRNG 算法的例子包括 Xoshiro256** 和 Xoroshiro128+。

描述

我们提供了一个新的接口 RandomGenerator,它为所有现有和新的 PRNG 提供了一个统一的 API。RandomGenerators 提供了名为 intslongsdoublesnextBooleannextIntnextLongnextDoublenextFloat 的方法,以及它们当前所有的参数变体。

我们提供了四个新的专门化 RandomGenerator 接口:

  • SplittableRandomGenerator 扩展了 RandomGenerator,并提供了名为 splitsplits 的方法。可分割性允许用户从现有的 RandomGenerator 派生一个新的 RandomGenerator,该 RandomGenerator 通常会产生统计上独立的结果。

  • JumpableRandomGenerator 扩展了 RandomGenerator,并提供了名为 jumpjumps 的方法。可跳跃性允许用户向前跳跃一定数量的抽取。

  • LeapableRandomGenerator 扩展了 RandomGenerator,并提供了名为 leapleaps 的方法。可长跳性允许用户向前跳跃大量的抽取。

  • ArbitrarilyJumpableRandomGenerator 扩展了 LeapableRandomGenerator,并提供了 jumpjumps 的额外变体,允许指定任意的跳跃距离。

我们提供了一个新的类 RandomGeneratorFactory,用于定位和构造 RandomGenerator 实现的实例。RandomGeneratorFactory 使用 ServiceLoader.Provider API 来注册 RandomGenerator 实现。

我们对 RandomThreadLocalRandomSplittableRandom 进行了重构,以便共享它们的大部分实现代码,并进一步使这些代码也可被其他算法重用。这一重构创建了底层的非公开抽象类 AbstractRandomGeneratorAbstractSplittableRandomGeneratorAbstractJumpableRandomGeneratorAbstractLeapableRandomGeneratorAbstractArbitrarilyJumpableRandomGenerator,每个类仅提供 nextInt()nextLong() 方法(以及相关的 split()jump()jump()leap()、或 jump(distance) 方法的实现)。重构后,RandomThreadLocalRandomSplittableRandom 继承了 RandomGenerator 接口。请注意,由于 SecureRandomRandom 的子类,因此 SecureRandom 的所有实例也自动支持 RandomGenerator 接口,无需对 SecureRandom 类或其任何关联的实现引擎进行重新编码。

我们还添加了扩展 AbstractSplittableRandomGenerator(因此实现 SplittableRandomGeneratorRandomGenerator)的底层非公开类,以支持 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 程序员在空间、时间、质量和与其他语言的兼容性之间提供了合理的权衡范围。

替代方案

我们考虑过仅引入新接口,而不改变 RandomThreadLocalRandomSplittableRandom 的实现。这有助于使 PRNG 对象更容易互换,但不会使它们的实现变得更容易。

我们还考虑过对 RandomThreadLocalRandomSplittableRandom 进行重构,而不改变它们的功能或添加任何新接口。我们认为这可以减少它们的总体内存占用,但不会对未来 PRNG 算法的实现或使用提供任何便利。

测试

  • 应继续使用针对 RandomThreadLocalRandomSplittableRandom 的所有现有测试。

  • 新测试(可能仅需执行一次):在修复新发现的两个弱点之前,应对重构后的 RandomThreadLocalRandomSplittableRandom 版本的输出进行抽查,以验证其行为是否保持不变,并与现有的(JDK 8)实现进行比较。

  • 新测试(可能仅需执行一次):应对 LXM 算法的输出进行抽查,并与用于使用 TestU01 和 PractRand 进行质量验证的 C 语言版本进行比较。

  • 新测试(将成为测试套件的永久部分):应测试 jump()leap() 方法,以验证它们是否按声明的距离遍历状态周期。例如,从任何特定初始状态开始,操作序列 nextLong(); jump() 应当使生成器处于与操作序列 jump(); nextLong() 相同的状态。

风险和假设

我们认为这是一个中等规模的项目,风险很小。最大的负担可能是制定规范,其次是测试。

我们已小心谨慎地确保遗留随机数生成器的行为未受影响。