为啥 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 标准库的实现决定的(例如,glibc
, libc.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::Rng”在 rand::thread_rng() 上调用 gen()?