【问题标题】:Why does java.util.Random use the mask?为什么 java.util.Random 使用掩码?
【发布时间】:2011-08-01 03:01:15
【问题描述】:

简化(即不考虑并发)Random.next(int bits) 看起来像

protected int next(int bits) {
    seed = (seed * multiplier + addend) & mask;
    return (int) (seed >>> (48 - bits));
}

使用掩码将种子减少到 48 位。为什么它比仅仅更好

protected int next(int bits) {
    seed = seed * multiplier + addend;
    return (int) (seed >>> (64 - bits));
}

?我读过很多关于随机数的文章,但看不出有什么原因。

【问题讨论】:

    标签: java random masking


    【解决方案1】:

    原因是较低的位往往具有较低的周期(至少在 Java 使用的算法中)

    来自Wikipedia - Linear Congruential Generator

    如上所示,LCG 并不总是使用它们产生的值中的所有位。 Java 实现每次迭代产生 48 位,但只返回这些值的 32 个最高有效位。这是因为高阶位的周期比低阶位长(见下文)。使用这种技术的 LCG 比不使用这种技术的 LCG 产生的价值要好得多。

    编辑:

    进一步阅读(方便地,在维基百科上)后,a、c 和 m 的值必须满足这些条件才能具有完整的句点:

    1. c 和 m 必须互质

    2. a-1 能被 m 的所有素因数整除

    3. 如果 m 是 4 的倍数,则 a-1 是 4 的倍数

    唯一我可以清楚地告诉我仍然满意的是#3。 #1 和 #2 需要检查,我感觉其中一个(或两个)都失败了。

    【讨论】:

    • 我知道高位确实更好,但这实际上是使用掩码的原因。选择的乘数和加数导致最大周期,这意味着没有屏蔽的周期将是 264 而不是 248。
    • @maartinus:你不能只把 48 改成 64;它改变了生成器的属性。
    • @Jason S 当然可以,但它会恶化什么吗?
    • 如果你不改变其他系数,它会破坏生成器。我不确定生成的生成器的周期是多少,但我怀疑您可能会得到一个小于 248 的周期——这需要一些仔细的数学分析才能显示它是什么。
    • @maaartinus:正如article cited中提到的,Java LCG的周期最多 m = 2**48。 @helloworld922:+1 这个 LCG 确实是 tested 并且低位的质量预期较差。
    【解决方案2】:

    来自 java.util.Random 顶部的文档:

    • 算法在计算机编程的艺术中有所描述,
    • Donald Knuth 的第 2 卷,第 3.2.1 节。它是一个 48 位种子,
    • 线性同余公式。

    因此,整个算法设计为使用 48 位种子,而不是 64 位种子。我想你可以和 Knuth 先生讨论一下;p

    【讨论】:

    • 我明白了,但是该公式适用于任何位长,而且这本书很旧。考虑到 8 位计算机的 Knuth 可能选择了 48 位种子。
    • '这本书很老了' -- 呵呵,数学没变。
    • @Jason S Math 并没有太多改变,但随着计算机速度的提高,它的使用确实发生了变化。
    • 算法中还有其他值需要更改,不能简单地交换不同的位数。算法中的a和c值需要调整。
    • 不,ac 可能保持不变,请参阅我对 helloworld922 的回答。
    【解决方案3】:

    来自wikipedia(@helloworld922 发布的引文所指的引文):

    LCG 的另一个问题是,如果 m 设置为 2 的幂,则生成序列的低位比特的周期比整个序列的周期短得多。通常,第 n 个最低有效位在输出序列的基 b 表示,其中对于某个整数 k,bk = m,最多以周期 bn 重复。

    此外,它还在继续(我的斜体):

    当 m 是 2 的幂时,LCG 的低位位不应依赖于任何程度的随机性。实际上,简单地用 2n 代替模数项表明低位比特经历了非常短的周期。特别是,当 m 是 2 的幂时,任何全周期 LCG 都会交替产生奇偶结果。

    最后,原因可能是历史原因:Sun 的人希望某些东西能够可靠地工作,而 Knuth 公式给出了 32 个有效位。请注意,java.util.Random API 是这样说的(我的斜体):

    如果使用相同的种子创建 Random 的两个实例,并且为每个实例进行相同的方法调用序列,它们将生成并返回相同的数字序列。 为了保证这个属性,为类 Random 指定了特定的算法。 Java 实现必须使用此处为类 Random 显示的所有算法,以保证 Java 代码的绝对可移植性。 但是,允许类 Random 的子类使用其他算法,只要它们遵守一般约定对于所有方法。

    所以我们坚持将其作为参考实现。但是,这并不意味着您不能使用另一个生成器(以及子类 Random 或创建一个新类):

    来自同一个维基百科页面:

    MMIX by Donald Knuth m=264 a=6364136223846793005 c=1442695040888963407

    有一个 64 位公式适合您。

    随机数很棘手(正如 Knuth 所说),根据您的需要,如果您需要 64 位数字,您可能只需调用两次 java.util.Random 并连接位即可。如果您真的关心统计属性,请使用Mersenne Twister,或者如果您关心信息泄漏/不可预测性,请使用java.security.SecureRandom

    【讨论】:

    • 您对签名的警告是错误的。只要不涉及超过 64 位的算术,符号性也与乘法无关。对于 64 位数字,有 Random.nextLong()
    • 啊——我的错,你是对的。 (证明: (aM)*(bM) mod M = a*b 其中 a 和 b 是无符号数, (aM) 和 (bM) 是 M = 2^64 的有符号数)我将删除该段。
    【解决方案4】:

    看起来没有这样做的理由。 使用经过验证的设计,应用掩码是一种保守的方法。 忽略它很可能会导致更好的生成器,但是,如果不熟悉数学,这是一个冒险的步骤。

    屏蔽的另一个小优势是 8 位架构的速度增益,因为它使用 6 个字节而不是 8 个字节。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-09-23
      • 2020-09-15
      • 2017-10-16
      • 1970-01-01
      • 2012-05-16
      • 2017-03-12
      相关资源
      最近更新 更多