为啥 rand() 在 Linux 上重复数字的频率远高于 Mac?

Posted

技术标签:

【中文标题】为啥 rand() 在 Linux 上重复数字的频率远高于 Mac?【英文标题】:Why does rand() repeat numbers far more often on Linux than Mac?为什么 rand() 在 Linux 上重复数字的频率远高于 Mac? 【发布时间】:2020-08-08 05:04:08 【问题描述】:

我在 C 中实现了一个哈希映射,作为我正在处理的项目的一部分,并使用随机插入来测试它。我注意到 Linux 上的 rand() 似乎比 Mac 上更频繁地重复数字。 RAND_MAX 在两个平台上都是 2147483647/0x7FFFFFFF。我已将其简化为这个测试程序,该程序生成一个字节数组RAND_MAX+1-long,生成RAND_MAX 随机数,记录每个是否重复,并将其从列表中检查出来。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int main() 
    size_t size = ((size_t)RAND_MAX) + 1;
    char *randoms = calloc(size, sizeof(char));
    int dups = 0;
    srand(time(0));
    for (int i = 0; i < RAND_MAX; i++) 
        int r = rand();
        if (randoms[r]) 
            // printf("duplicate at %d\n", r);
            dups++;
        
        randoms[r] = 1;
    
    printf("duplicates: %d\n", dups);

Linux 持续生成大约 7.9 亿个重复项。 Mac 始终只生成一个,因此它循环遍历它可以生成的每个随机数几乎而不重复。谁能向我解释这是如何工作的?我无法分辨出与man 页面有什么不同,无法分辨每个人使用的是哪个RNG,也无法在网上找到任何东西。谢谢!

【问题讨论】:

由于 rand() 从 0..RAND_MAX (含)返回值,因此您的数组需要调整大小为 RAND_MAX+1 您可能已经注意到 RAND_MAX/e ~= 7.9 亿。当 n 接近无穷大时,(1-1/n)^n 的极限也是 1/e。 @DavidSchwartz 如果我理解正确,这可以解释为什么 Linux 上的数字始终在 7.9 亿左右。我想问题是:Mac为什么/如何重复那么多次? 运行时库中的 PRNG 没有质量要求。唯一真正的要求是相同种子的可重复性。 显然,Linux 中的 PRNG 质量比 Mac 中的要好。 @chux 是的,但由于它基于乘法,因此状态永远不会为零,否则结果(下一个状态)也将为零。根据源代码,如果以零为种子,它会检查零作为一种特殊情况,但它永远不会产生零作为序列的一部分。 【参考方案1】:

虽然一开始听起来 macOS rand() 在不重复任何数字方面更好,但应该注意的是,在生成这么多数字的情况下,expected 会看到很多重复(实际上,大约 790百万,或 (231-1)/e)。同样,按顺序遍历数字也不会产生重复,但不会被认为是非常随机的。因此,Linux rand() 实现在此测试中与真正的随机源无法区分,而 macOS rand() 则不然。

另一件乍看起来令人惊讶的事情是 macOS rand() 如何能够很好地避免重复。查看its source code,我们发现实现如下:

/*
 * Compute x = (7^5 * x) mod (2^31 - 1)
 * without overflowing 31 bits:
 *      (2^31 - 1) = 127773 * (7^5) + 2836
 * From "Random number generators: good ones are hard to find",
 * Park and Miller, Communications of the ACM, vol. 31, no. 10,
 * October 1988, p. 1195.
 */
    long hi, lo, x;

    /* Can't be initialized with 0, so use another value. */
    if (*ctx == 0)
        *ctx = 123459876;
    hi = *ctx / 127773;
    lo = *ctx % 127773;
    x = 16807 * lo - 2836 * hi;
    if (x < 0)
        x += 0x7fffffff;
    return ((*ctx = x) % ((unsigned long) RAND_MAX + 1));

这确实会导致 1 和 RAND_MAX 之间的所有数字,包括在内,恰好一次,然后序列再次重复。由于下一个状态基于乘法,因此状态永远不会为零(或所有未来状态也将为零)。因此,您看到的重复数字是第一个数字,而零是永远不会返回的数字。

至少只要 macOS(或 OS X)存在,Apple 就一直在其文档和示例中推广使用更好的随机数生成器,因此 rand() 的质量可能并不重要,而且他们我只是坚持使用可用的最简单的伪随机生成器之一。 (正如您所指出的,他们的rand() 甚至被评论为建议使用arc4random()。)

在相关的说明中,我能找到的最简单的伪随机数生成器在这个(以及许多其他)随机性测试中产生了不错的结果是xorshift*:

uint64_t x = *ctx;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
*ctx = x;
return (x * 0x2545F4914F6CDD1DUL) >> 33;

此实现导致您的测试中几乎正好有 7.9 亿次重复。

【讨论】:

1980 年代发表的journal article 提出了基于“生日问题”的 PRNG 统计测试。 “Apple 一直在他们的文档中推广使用更好的随机数生成器”--> 当然,Apple 可以使用 arc4random()rand() 后面的代码并获得良好的 rand() 结果。与其试图引导程序员以不同的方式编写代码,不如创建更好的库函数。 “他们只是卡住了”是他们的选择。 mac 的rand() 中缺少常量偏移量使其非常糟糕,以至于在实际使用中没有用处:Why does rand() % 7 always return 0?, Rand() % 14 only generates the values 6 or 13 @PeterCordes:rand 有这样的要求,即用相同的种子重新运行它会产生相同的序列。 OpenBSD 的rand 已损坏且不遵守此合同。 @R..GitHubSTOPHELPINGICE 您是否看到 C 要求 rand() 使用相同的种子在不同版本的库之间产生相同的序列?这样的保证可能对库版本之间的回归测试有用,但我发现它没有 C 要求。【参考方案2】:

MacOS 在 stdlib 中提供了一个未记录的 rand() 函数。如果不设置种子,则它输出的第一个值是 16807、282475249、1622650073、984943658 和 1144108930。quick search 将显示此序列对应于一个非常基本的 LCG 随机数生成器,该生成器迭代以下公式:

xn+1 = 75 · xn (mod 231 - 1)

由于这个RNG的状态完全由单个32位整数的值来描述,所以它的周期不是很长。准确地说,它每 231 - 2 次迭代重复一次,输出从 1 到 231 - 2 的每个值。

我不认为所有版本的 Linux 都有 rand() 的标准实现,但有一个经常使用的 glibc rand() function。这不是使用单个 32 位状态变量,而是使用超过 1000 位的池,从所有意图和目的来看,它永远不会产生完全重复的序列。同样,您可以通过打印此 RNG 的前几个输出来找出您拥有的版本,而无需先播种。 (glibc rand() 函数产生数字 1804289383、846930886、1681692777、1714636915 和 1957747793。)

因此,您在 Linux 中遇到更多冲突(而在 MacOS 中几乎没有)的原因是 Linux 版本的 rand() 基本上更加随机。

【讨论】:

未播种的rand() 的行为必须与srand(1); 相同 macOS 中rand() 的源代码可用:opensource.apple.com/source/Libc/Libc-1353.11.2/stdlib/FreeBSD/… FWIW,我对从源代码编译的这个进行了相同的测试,它确实只导致了一个重复。 Apple 在他们的示例和文档中一直在推广使用其他随机数生成器(例如 arc4random() 在 Swift 接管之前),因此 rand() 的使用在他们平台上的原生应用程序中可能不是很常见,这可以解释为什么它没有更好。 感谢您的回复,这回答了我的问题。一段 (2^31)-2 解释了为什么它会像我观察到的那样在最后开始重复。你(@r3mainer)说rand() 没有记录,但@Arkku 提供了指向明显来源的链接。你们谁知道为什么我在我的系统上找不到那个文件,为什么我在 Mac 的stdlib.h 中只看到int rand(void) __swift_unavailable("Use arc4random instead.");?我想@Arkku 链接到的代码刚刚编译到...什么库? @TheronS 编译成C库,libc,/usr/lib/libc.dylib。 =) 给定 C 程序使用哪个版本的rand() 不是由“编译器”或“操作系统”决定的,而是由 C 标准库的实现决定的(例如,glibclibc.dylib, msvcrt*.dll)。【参考方案3】:

rand() 是由 C 标准定义的,C 标准并没有指定使用哪种算法。显然,Apple 使用的算法比您的 GNU/Linux 实现低:在您的测试中,Linux 与真正的随机源无法区分,而 Apple 实现只是将数字打乱。

如果你想要任何质量的随机数,要么使用更好的 PRNG,它至少可以保证它返回的数字的质量,或者简单地从 /dev/urandom 或类似的地方读取。后者为您提供加密质量数字,但速度很慢。即使它本身太慢,/dev/urandom 也可以为其他更快的 PRNG 提供一些优秀的种子。

【讨论】:

感谢您的回复。我实际上并不需要一个好的 PRNG,只是担心我的 hashmap 中潜伏着一些未定义的行为,然后当我消除这种可能性并且平台仍然表现不同时感到好奇。 顺便说一句,这是一个加密安全随机数生成器的示例:github.com/divinity76/phpcpp/commit/… - 但它是 C++ 而不是 C,我让 STL 实现者完成所有繁重的工作.. @hanshenrik 对于一个简单的哈希表来说,加密 RNG 通常是多余的,而且太慢了。 @PM2Ring 绝对。哈希表哈希主要需要快速,而不是好。但是,如果您想开发一种不仅速度快而且还不错的哈希表算法,我相信了解一些密码哈希算法的技巧是有益的。它将帮助您避免大多数最明显的错误,这些错误困扰着最快速的哈希算法。不过,我不会在这里宣传具体的实现。 @cmaster 确实如此。了解mixing functions 和avalanche effect 之类的东西当然是个好主意。幸运的是,有一些非加密哈希函数具有良好的属性,不会牺牲太多速度(如果正确实施),例如 xxhash、murmur3 或 siphash。【参考方案4】:

一般而言,rand/srand 对已被认为已被弃用很长时间,因为低位位在结果中显示的随机性低于高位位。这可能与您的结果有任何关系,也可能没有任何关系,但我认为这仍然是一个很好的机会来记住,即使一些 rand/srand 实现现在更新了,旧的实现仍然存在,最好使用 random(3 )。在我的 Arch Linux 机器上,下面的注释仍在 rand(3) 的手册页中:

  The versions of rand() and srand() in the Linux C Library use the  same
   random number generator as random(3) and srandom(3), so the lower-order
   bits should be as random as the higher-order bits.  However,  on  older
   rand()  implementations,  and  on  current implementations on different
   systems, the lower-order bits are much less random than the  higher-or-
   der bits.  Do not use this function in applications intended to be por-
   table when good randomness is needed.  (Use random(3) instead.)

在其下方,手册页实际上提供了非常简短、非常简单的 rand 和 srand 示例实现,它们是关于您所见过的最简单的 LC RNG,并且具有较小的 RAND_MAX。我认为它们与 C 标准库中的内容不匹配,如果有的话。或者至少我希望不会。

一般来说,如果您要使用标准库中的某些内容,请尽可能使用 random(手册页将其列为 POSIX 标准回到 POSIX.1-2001,但 rand 是 C 之前的标准方式甚至标准化)。或者更好的是,打开数字食谱(或在线查找)或 Knuth 并实施一个。它们非常简单,您只需要执行一次即可拥有具有您最需要的属性且质量已知的通用 RNG。

【讨论】:

感谢您的上下文。我实际上并不需要高质量的随机性,并且已经实现了 MT19937,尽管是在 Rust 中。主要是好奇如何找出这两个平台的行为不同的原因。 有时最好的问题是出于简单的兴趣而不是严格的需要而提出的——似乎这些问题往往会从特定的好奇心点得到一套好的答案。你的就是其中之一。献给所有好奇的人,真正的原创黑客。 有趣的是,建议是“停止使用 rand()”而不是让 rand() 变得更好。标准中没有规定它必须是特定的生成器。 @pipe 如果让rand()“更好”意味着让它变慢(它可能会——加密安全的随机数需要付出很多努力),那么保持它的速度可能会更好,即使如果稍微更可预测的话。举个例子:我们有一个生产应用程序需要很长时间才能启动,我们追踪到一个 RNG,它的初始化需要等待足够的熵生成……结果它不需要那么安全,所以用“更糟糕”的 RNG 是一个很大的改进。

以上是关于为啥 rand() 在 Linux 上重复数字的频率远高于 Mac?的主要内容,如果未能解决你的问题,请参考以下文章

EXCEL,怎么让随机数据rand函数的数据随机范围在单元格数字范围之内?

为啥 rand() + rand() 会产生负数?

为啥我需要“使用 rand::Rng”在 rand::thread_rng() 上调用 gen()?

rand产生随机数怎样控制在1~52内而且不能重复。1~52必须出现一次。谢谢

为啥 rand()%6 有偏见?

Excel生成不重复的8位随机码