为什么rand()在Linux上重复的次数比Mac重复得多?


86

当我发现rand()Linux上的重复次数似乎比Mac上重复的次数要多时,我正在C中实现一个哈希图,这是我正在研究的项目的一部分,并使用随机插入进行测试。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始终只生成一个,因此它遍历几乎可以重复生成的每个随机数。谁能告诉我这是如何工作的?我无法分辨出与手册页不同的东西,无法分辨每个正在使用的RNG,也无法在线找到任何东西。谢谢!


4
由于rand()返回的值从0..RAND_MAX(含)起,因此您需要调整数组的大小RAND_MAX + 1
Blastfurnace

21
您可能已经注意到RAND_MAX / e〜= 7.9亿。同样,当n接近无穷大时,(1-1 / n)^ n的极限为1 / e。
David Schwartz

3
@DavidSchwartz如果我对您的理解正确,则可以解释为什么Linux上的数字始终约为7.9亿。我想接下来的问题是:为什么/如何Mac的重复多少次?
Theron S

26
在运行时库中对PRNG的质量没有要求。只有真正的要求是在相同种子下具有可重复性。显然,Linux中PRNG的质量要好于Mac。
pmg

4
@chux是的,但是由于它基于乘法,所以状态永远不能为零,否则结果(下一个状态)也将为零。根据源代码,如果用零作为种子,它确实会检查零,但它不会在序列中产生零。
Arkku

Answers:


118

虽然一开始听起来好像macOS rand()对于不重复任何数字来说会更好一些,但应该注意的是,生成如此大量的数字后,它有望看到大量重复(事实上,大约有7.9亿,或者(2 31 -1 )/ e)。同样,依次遍历数字也不会产生重复,但不会被认为是非常随机的。因此,在此测试中,Linux的rand()实现与真正的随机源是无法区分的,而macOS 则不是。rand()

乍看起来似乎令人惊讶的另一件事是macOS rand()如何设法很好地避免重复。查看其源代码,我们发现实现如下:

/*
 * 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));

实际上RAND_MAX,在序列再次重复之前,确实会导致1到0之间的所有数字恰好一次。由于下一个状态是基于乘法的,因此该状态永远不能为零(否则所有将来的状态也将为零)。因此,您看到的重复数字是第一个数字,零是永不返回的数字。

至少在存在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亿重复。


5
期刊文章发表在1980年提出了基于“生日问题”的PRNG统计检验。
pjs

14
“ Apple一直在其文档中推广使用更好的随机数生成器”->当然,Apple可以采用arc4random()类似的代码rand()并获得良好的rand()结果。与其尝试引导程序员进行不同的编码,不如创建更好的库函数。他们选择“他们刚刚卡住”。
chux-恢复莫妮卡

22
Mac中缺少恒定的偏移量rand()使情况变得如此糟糕,以至于在实际使用中没有用:为什么rand()%7总是返回0?Rand()%14仅生成值6或13
phuclv

4
@PeterCordes:对的要求rand如此,以同一种子重新运行会产生相同的序列。OpenBSD的rand损坏,不遵守该合同。
R .. GitHub停止帮助ICE

8
@ R..GitHubSTOPHELPINGICE您是否看到C要求,即rand()具有相同种子的C 在不同版本的库之间产生相同的序列?这样的保证对于库版本之间的回归测试可能很有用,但是我发现它没有C要求。
chux-恢复莫妮卡

33

MacOS在stdlib中提供了未公开的rand()函数。如果不使用它,则其输出的第一个值是16807、282475249、1622650073、984943658和1144108930。快速搜索将显示此序列对应于一个非常基本的LCG随机数生成器,该生成器会迭代以下公式:

x n +1 = 7 5 · x n(mod 2 31 − 1)

由于此RNG的状态完全由单个32位整数的值描述,因此其周期不是很长。确切地说,它每2 31 − 2次迭代重复一次,输出从1到2 31 − 2的每个值。

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

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


5
一个没有种子的人的rand()行为必须与srand(1);
pmg

5
rand()macOS中的源代码可用:opensource.apple.com/source/Libc/Libc-1353.11.2/stdlib/FreeBSD / ... FWIW,我对从源代码编译的该文件进行了相同的测试,确实导致了只有一个副本。Apple一直arc4random()在其示例和文档中推广使用其他随机数生成器(例如,在Swift接手之前),因此rand()在其平台上的本机应用程序中使用可能不是很普遍,这也许可以解释为什么它并不更好。
Arkku

感谢您的答复,这回答了我的问题。周期(2 ^ 31)-2解释了为什么它会像我观察到的那样从头开始重复。您(@ r3mainer)说rand()没有记录,但是@Arkku提供了到明显来源的链接。你们两个都知道为什么我无法在系统上找到该文件,以及为什么只能int rand(void) __swift_unavailable("Use arc4random instead.");在Mac的系统中看到stdlib.h吗?我想链接到@Arkku的代码只是编译成...什么库?
Theron S

1
@TheronS它将被编译到C库libc中/usr/lib/libc.dylib。=)
Arkku

5
哪个版本的rand()一个给定的C程序的用途不被“编译”或“操作系统”,而是C标准库的实现来确定(例如glibclibc.dylibmsvcrt*.dll)。
Peter O.

10

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

如果您想要任意质量的随机数,请使用更好的PRNG,以便至少对返回的数字的质量提供某些保证,或者只是从中读取/dev/urandom或类似的数字。后者为您提供加密质量数字,但速度较慢。即使它本身太慢,/dev/urandom也可以为其他更快的PRNG提供一些出色的种子。


谢谢回复。我实际上并不需要一个好的PRNG,只是担心哈希表中潜伏着一些未定义的行为,然后当我消除这种可能性并且平台的行为仍然不同时感到好奇。
Theron S

顺便说一句这里的加密安全随机数生成器的例子:github.com/divinity76/phpcpp/commit/... -但它的C ++而不是C,我让STL实现者做所有的繁重..
hanshenrik

3
@hanshenrik对于简单的哈希表,加密的RNG通常是多余的并且太慢。
2

1
@ PM2Ring绝对。哈希表哈希主要需要快速但不好。但是,如果您想开发一种不仅速度快而且还不错的哈希表算法,我相信了解密码哈希算法的一些技巧是有益的。它可以帮助您避免困扰最快速的哈希算法的大多数最明显的错误。不过,我不会在这里做广告宣传特定的实现。
cmaster-恢复莫妮卡

@cmaster足够正确。了解一些诸如混合函数雪崩效应之类的东西当然是个好主意。幸运的是,有一些具有良好属性的非加密哈希函数,它们不会牺牲太多速度(如果正确实现),例如xxhash,murmur3或siphash。
2

5

通常,由于结果中的低阶位显示的随机性低于高阶位,因此很长时间以来就已将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标准库中的内容。或者至少我希望不会。

通常,如果您要使用标准库中的内容,则可以使用随机数(手册页将其列为POSIX标准,回到POSIX.1-2001,但rand是C标准化之前的标准方式) 。或者更好的方法是,打开数字食谱(或在线查找)或Knuth并实施一个。它们真的很容易,您只需要做一次就可以拥有具有您最需要的属性且具有已知质量的通用RNG。


感谢您提供的上下文。我实际上不需要高质量的随机性,并且已经在Rust中实现了MT19937。大多数人只是好奇如何找出两个平台为何表现不同。
Theron S

1
有时最好的问题是出于简单的兴趣而不是严格的需求而提出的-似乎这些问题往往是出于好奇的特定目的而产生的。您的就是其中之一。这是所有好奇的人,真正的黑客和原始黑客。
Thomas Kammeyer,

有趣的是,建议是“停止使用rand()”而不是使rand()更好。标准中没有任何内容表明它必须是特定的生成器。
管道

2
@pipe如果使rand()“更好”意味着降低它的速度(可能会这样做(可能需要付出很多努力-加密安全的随机数),那么即使稍微更可预测,也最好使其保持更快。恰当的例子:我们有一个生产应用程序,它花了很长时间才能启动,我们追溯到一个RNG,该RNG的初始化需要等待足以产生足够的熵……事实证明,它并不需要那么安全,因此将其替换为“更差”的RNG是一个很大的进步。
gidds
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.