【问题标题】:Looking for decent-quality PRNG with only 32 bits of state寻找只有 32 位状态的高质量 PRNG
【发布时间】:2013-06-11 01:59:02
【问题描述】:

我正在尝试实现rand_r 接口的可容忍质量版本,它有一个不幸的接口要求,即它的整个状态存储在unsigned 类型的单个对象中,对我而言,这意味着正好32位。另外,我需要它的输出范围是[0,2³¹-1]。标准解决方案是使用 LCG 并丢弃低位(周期最短),但这仍然会为接下来的几个位留下非常糟糕的周期。

我最初的想法是使用 LCG 的两个或三个迭代来生成输出的高/低或高/中/低位。然而,这种方法并不能保持无偏分布;不是每个输出值都具有相同的频率,而是多次出现,而有些则根本不出现。

由于只有 32 位状态,PRNG 的周期以 2³² 为界,并且为了无偏,如果 PRNG 具有完整周期,则每个值必须恰好输出两次,如果具有完整周期,则必须恰好输出一次周期 2³¹。较短的周期不能没有偏见。

是否有任何知名的 PRNG 算法符合这些标准?

【问题讨论】:

  • 保证输出似乎与随机相反,不是吗?
  • 伪随机不是随机的。不幸的是,“保证产出”的反面是“有偏见的”。对于像 2^100 这样非常大的时期,只要在经验上与均匀无法区分,那么具有完美输出分布并不是真正必要的,但对于像 2^32 这样的小时期,非均匀性将是一个明显的可观察到的偏差。
  • 是的,如果之前返回的值有大约 0.5 GB 的数据,您可以预测周期为 2^32 的 PRNG 的下一个值。这是意料之中的,即使分布不均匀也是如此。如果恰好有 2^32 个状态和 2^32 个输出,并且您已经看到其中的 2^32-1 个,则始终确定最后一个。
  • 我删除了我的评论。我不太明白你在说什么,但我想我现在明白了。
  • 如何使用 LCG,然后应用安全的 1:1 转换(我建议了一些 here)来改进分布?原则上,这类似于在计数器模式下运行加密算法。

标签: c random prng


【解决方案1】:

提供非常高质量的一个很好(但可能不是最快)的可能性是在CTR mode 中使用32 位block cipher。基本上,您的 RNG 状态只是一个 32 位计数器,每次调用 RNG 时都会加一,输出将是使用带有任意选择的固定密钥的分组密码对该计数器值进行加密。对于额外的随机性,您甚至可以提供一个(非标准)函数让用户设置自定义键。

常用的 32 位块密码并不多,因为如此短的块大小会给加密使用带来问题。 (基本上,birthday paradox 可以让您在大约 216 = 65536 个输出和 232 输出的非随机性显然变得确定了。)但是,一些具有可调整块大小的密码,例如 XXTEAHPC,会让你下降到 32 位,并且应该适合您的目的。

(编辑:我的错,XXTEA 只能降到 64 位。但是,正如 CodesInChaos 在 cmets 中所建议的那样,Skip32 可能是另一种选择。或者您可以构建自己的 32-位Feistel cipher.)

CTR 模式构造保证 RNG 将具有 232 个输出的完整周期,而(未损坏的)分组密码的标准安全声明本质上是它在计算上不可行将它们的输出与 32 位整数集的 random permutation 区分开来。 (当然,如上所述,这种排列仍然很容易与采用 32 位值的 random function 区分开来。)

使用 CTR 模式还提供了一些您可能会觉得方便的额外功能(即使它们不是您正在开发的官方 API 的一部分),例如能够快速查找 RNG 输出流中的任何点通过从状态中增加或减少。

另一方面,您可能希望通过仅将内部状态设置为种子值来遵循播种 RNG 的常见做法,因为这会导致从附近的种子高度相似(基本上只是相同的流因种子的差异而移动)。避免此问题的一种方法是在播种过程中添加额外的加密步骤,即使用密码加密种子并将内部计数器值设置为等于结果。

【讨论】:

  • 这种方法的问题是确保完整的周期。显然,它可以在大约一天的计算时间下进行经验测试,但我认为没有明显的理由相信应用少于 2³² 次的密码不会返回原始值。
  • CTR 模式构造明确保证完整周期:输出流只会在计数器回绕时重复。
  • 哦,我明白了。不幸的是,API 没有播种过程。种子和状态是相同的。这是一个非常、非常糟糕的 API,这就是我如此受限的原因。话虽如此,我认为它可以加密输入种子/状态,使用获得的值作为结果,将该值增加 1,然后“解密”它并将结果存储回种子/状态。
  • 您还可以将 CTR 模式中的计数器替换为“通用计数器”,即任何 32 位值的全周期序列(例如全周期 LCRNG)。这样一来,您实际上是在使用分组密码通过加密其输出来加强更简单的全周期 RNG。底层的简单 LCRNG 仍应足够随机,以避免顺序种子值出现问题。
  • 根据维基百科 XXTEA 的最小块大小为 64 位。您可能需要考虑 Skip32。
【解决方案2】:

32 位最大周期 Galois LFSR 可能适合您。试试:

r = (r >> 1) ^ (-(r & 1) & 0x80200003);

LFSR 的一个问题是你不能产生值 0。所以这个值的范围是 1 到 2^32-1。您可能想要调整输出或坚持使用良好的 LCG。

【讨论】:

  • 或许if (r==0xdeadbeef) r=0; else if (r==0) r=0xdeadbeef; r = (r >> 1) ^ (-(r & 1) & 0x80200003);
  • 如果我们把它设为零,它就是零。诀窍是每 2^31-1 次迭代进出零状态一次。这样,将零添加到输出集,将其精确扩展到 2^32,并使其完全无偏。 0xdeadbeef 只是一个任意值,之后将 0 插入到流中(如果我们运行生成器并不重要,因为零变为零)。下一次我们看到状态为零,所以我们跳回到上次跳出的同一个地方,继续前进,就好像什么都没发生一样。我建议的更改只是为了提高计算效率。
  • 啊,我明白了。您在任意点将 0 插入序列中。凉爽的。花了我一段时间。
  • @MichelRouzic 它不会用零替换一个值,它会在该值之后的序列中插入一个零,给出一个长度为 2^32 的序列。其实很聪明。
  • 啊,没关系,我发现了我的问题。它使用的是无符号整数,并且弄乱了代数的负数部分。
【解决方案3】:

除了使用Lehmer MCG,您还可以使用一对:

Xorshift 的 32 位变体在 32 位状态下的保证周期为 232-1

uint32_t state;

uint32_t xorshift32(void) {
    state ^= state << 13;
    state ^= state >> 17;
    state ^= state << 5;
    return state;
}

这是 2003 年最初的 32 位建议(见论文)。根据您对“体面的质量”的定义,那应该没问题。然而,它没有通过Diehard 的二进制等级测试和 SmallCrush 的 5/10 测试。

具有更好混合和常数的替代版本(通过 SmallCrush 和 Crush)

uint32_t xorshift32amx(void) {
    int s = __builtin_bswap32(state * 1597334677);
    state ^= state << 13;
    state ^= state >> 17;
    state ^= state << 5;
    return state + s;
}

基于研究herehere


还有Mulberry32,它的句点正好 232

uint32_t mulberry32(void) {
    uint32_t z = state += 0x6D2B79F5;
    z = (z ^ z >> 15) * (1 | z);
    z ^= z + (z ^ z >> 7) * (61 | z);
    return z ^ z >> 14;
}

这可能是您最好的选择。这是相当不错。作者说“它通过了 gjrand 的 13 次测试,没有失败,总 P 值 在 4GB 生成的数据。这是整个周期的四分之一”。它似乎比 SplitMix32 有所改进。


“SplitMix32”,取自 xxHash/MurmurHash3(Weyl 序列)

uint32_t splitmix32(void) {
    uint32_t z = state += 0x9e3779b9;
    z ^= z >> 15; // 16 for murmur3
    z *= 0x85ebca6b;
    z ^= z >> 13;
    z *= 0xc2b2ae35;
    return z ^= z >> 16;
}

这里的质量可能有问题,但它的 64 位大哥有一个 lot of fans(通过 BigCrush)。所以大体结构值得一看。

【讨论】:

  • 对 PCG 家族有什么想法吗? (例如,PCG 论文中称为“PCG RXS M XS 32 (LCG)”的生成器。)
  • @MarkDickinson 据我所知,PCG 依赖于 64 位数学,并且我找不到完全 32 位的变体。我也找不到任何特定于您提到的生成器的代码。我确信 PCG 是一个很好的生成器,但我觉得它对于 JavaScript 或嵌入式系统来说是一个糟糕的选择。例如xoshiro128** 在这方面是理想的,因为它仅使用 32 位整数。
  • 有道理;谢谢。当然,您可以模拟所需的 64 位算法,但这会影响代码的复杂性和效率。
  • splitmix32 的代码没有 2^32 的句点。它在 0xe1aff528 有一个固定点!此外,mulberry32 也没有 2^32 的周期(尽管至少它没有固定点)。
  • 您确定 Mulberry32 吗?作者声称 “它应该有一个 2 到 32 的周期,确切地说,因为在循环之前状态会变为每个 32 位无符号整数。”。你的测试代码是什么?
【解决方案4】:

详细说明我的评论...

计数器模式下的分组密码以大致以下形式提供生成器(使用更大的数据类型除外):

uint32_t state = 0;
uint32_t rand()
{
    state = next(state);
    return temper(state);
}

由于尚未指定加密安全性(并且在 32 位中它或多或少是无用的),因此应该使用更简单的临时调节函数来解决问题。

一种方法是,next() 函数很简单(例如,return state + 1;),而temper() 通过复杂(如分组密码)进行补偿。

一种更平衡的方法是在next() 中实现 LCG,因为我们知道它也会访问所有可能的状态,但以随机(ish)顺序,并找到 temper() 的实现,它所做的工作足够涵盖 LCG 的剩余问题。

Mersenne Twister 在其输出中包含这样的回火功能。那可能是合适的。此外,this question 要求满足要求的操作。

我有一个最喜欢的方法,就是对单词进行位反转,然后将它乘以某个常数(奇数)数。如果位反转不是您架构上的本机操作,那可能会过于复杂。

【讨论】:

  • 我没有尝试过位反转,但到目前为止字节交换似乎很有希望。 LFSR 作为“下一个”功能看起来比 LCG 更有希望,因为 LCG 在其低位中的周期非常糟糕,而 LFSR 在所有位中都有完整的周期。
  • @R..,这个想法是temper() 会传播东西,所以坏周期位被分散。 LCG 只是具有全周期的简单优势,没有任何额外的复杂性。但是,事实上,如果您确实在回火中使用了乘法,那么最好为next() 使用一种根本不同的算法。所以也许我同意你的看法。
  • 我刚刚尝试了一个带有从 mersenne twister 复制的 temp 函数的幼稚 LCG,而顽固的​​结果在统计上与 /dev/urandom 没有区别。 :-)
  • @R.., 然后test harder!
  • @R..,你尝试过更难的测试吗?我不指望他们会出现任何新的东西,但这似乎太容易了;好像顽固分子忽略了什么。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-04-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-25
  • 1970-01-01
  • 2018-03-31
相关资源
最近更新 更多