#random #numbers #u64 #generator #non-cryptographic #high #performance

无 std 蒲公英-随机数生成器

一个高性能的非加密随机数生成器

2 个版本

0.1.1 2024年6月26日
0.1.0 2024年6月26日

#674 in 算法

艺术-2.0

22KB
315

蒲公英是一个适用于模拟、测试、统计和数据结构算法的高性能非加密随机数生成器。它具有128位的态,周期长度为2¹²⁸ - 1。

示例

use dandelion::Rng;
let mut rng = Rng::from_u64(0);
let x = rng.u64();
let y = rng.between_u64(1, 6);
let z = rng.f64();

算法

蒲公英随机数生成器的核心包括两部分:状态转换函数 F: (u64, u64) -> (u64, u64) 和输出函数 G: (u64, u64) -> u64,其中第k个输出是

G(F(F( ... F(x₀, y₀) ... )))
  \________/
   k times

其中初始状态为 (x₀, y₀)

状态转换函数 F 定义为

F(x, y) = (y ^ shr(y, 19), x ^ ror(y, 7))

输出函数 G 定义为

G(x, y) = y + ((x * x) ^ ((x * x) >> 64))

其中 xyu64,我们使用了一个 (u64, u64) -> u128 全乘法。我们进一步要求状态不能全为零,即 (x, y)(0, 0)

关于这些函数,有两点需要注意。首先,F 可以被视为作用在 128 位向量空间 F₂¹²⁸ 上的可逆线性变换,即 F 是 GL(128, F₂) 的一个元素。其次,G 可以被视为 F₂¹²⁸ / { 0 } 上的一个排列,然后进行截断。

状态转换函数的周期

我们将证明状态转换函数的周期为 2¹²⁸ - 1。我们可以在 GL(128, F₂) 中的二进制矩阵上进行必要的计算。设 A 为表示我们的转换的二进制矩阵。那么,只需要证明

pow(A, 2¹²⁸ - 1) = I

pow(A, (2¹²⁸ - 1) / p) ≠ I

对于 2¹²⁸ - 1 的所有素数因子 p,其中 I 是单位矩阵。我们可以在 O(log(n)) 时间内计算 pow(A, n),因此这些计算是可行的。实际上,检查等价的 pow(A, 2¹²⁸) = A 对于第一个条件要快一些。

Mersenne 数 2¹²⁸ - 1 可以分解为 Fermat 数的乘积

2¹²⁸ - 1 =
    (2¹ + 1)
    * (2² + 1)
    * (2⁴ + 1)
    * (2⁸ + 1)
    * (2¹⁶ + 1)
    * (2³² + 1)
    * (2⁶⁴ + 1)

而较小的 Fermat 数的素因子分解是众所周知的。2¹²⁸ - 1 的完整素因子分解为

2¹²⁸ - 1 =
    3
    * 5
    * 17
    * 257
    * 65_537
    * 641
    * 6_700_417
    * 274_177
    * 67_280_421_310_721

满足

(x, y) ⇒ (y ^ shr(y, α), x ^ ror(y, β))

具有完整周期的 (α, β) 的完整集合是

α=19 β=7
α=29 β=23 
α=33 β=29 

我们使用 (19, 7) 因为它具有最稀疏的转换矩阵。

最大等分布

由于状态转换函数具有完整周期且输出函数是一个截断排列,随机数生成器的最大 1-等分布的事实是显而易见的。

实际上,Feistel 轮是一个排列,与 H 的任何属性无关。

(x, y) ⇒ (y + H(x), x)

输出多重集是 F₂¹²⁸ / 0 截断到 64 位,因此 0 出现 2⁶⁴ - 1 次,其他每个值出现 2⁶⁴ 次。

设计考虑

选择 128 位状态大小是因为 64 位可能对于某些非加密应用来说太小,但 128 位对于几乎所有事情都应该足够。

状态转换函数在精神上与 xorshift 扩展家族中的那些相似,但尽可能弱化,同时保持完整周期。

输出函数包含一个混合器,它执行 (u64, u64) -> u128 全乘法,然后异或上下半部分。这执行了大量混合,同时在现代手机、笔记本电脑和服务器上的 CPU 上相对便宜。这种混合器的灵感来自于 mum-hash 和 wyhash 库中的类似结构。

依赖项

~135KB

可选功能