为什么使用rand()被认为是不好的?

14

尽管使用 srand() 种子,但通常不建议使用 rand()。为什么会这样呢?有哪些更好的替代方案可用?


你可以使用例如 std::random_device 这样的工具,它可以与许多数字分布一起使用。 - Rafał Górczewski
当我使用srand(time(NULL))时,种子会改变,但仍不建议使用它。为什么呢? - Uncertainly Certain
1
这个视频有点夸张了问题,但它很好地解释了rand()存在的一些问题。 - 463035818_is_not_a_number
@Sid time(NULL) 每秒钟都会变化。如果您每秒运行它多次,您将得到相同的结果。 - VLL
@463035818_is_not_a_number 这个链接是404错误。也许这个是视频,也许不是,但它与主题相关且是一个好的视频。 - Quirin F. Schroll
7个回答

23

这个故事分为两个部分。

首先,rand是一个伪随机数生成器,这意味着它依赖于一个种子。对于给定的种子,它总是会产生相同的序列(假设实现相同)。这使得它不适合某些需要高度安全性的应用程序。 但是这并不是特定于rand的问题。任何伪随机生成器都存在此问题。而且在许多情况下,使用伪随机生成器是可接受的,因为真正的随机生成器也存在其自身的问题(效率、实现、熵)。

所以你分析了问题,并得出结论:伪随机生成器是解决方案。然后我们来到了与 C 随机库(包括randsrand)有关的真正麻烦的问题,这些问题是特定于它的,使其过时(也就是说,你永远不应该使用rand和C随机库的原因)。

  • 问题之一是它具有全局状态(由srand设置)。这使得同时使用多个随机引擎变得不可能。这也大大复杂化了多线程任务。

  • IT技术最显著的问题是缺乏分发引擎rand给出一个在区间[0,RAND_MAX]内的数字。它在这个区间内是均匀的,这意味着在这个区间内每个数字出现的概率相同。但通常你需要一个特定区间内的随机数。比如[0, 1017]。常见(且天真)使用的公式是rand() % 1018。但问题是,除非RAND_MAX正好是1018的倍数,否则你不会得到均匀分布。

  • 另一个问题是rand的实现质量。有其他答案更好地详细说明了这一点,请阅读它们。

在现代C++中,你应该绝对使用来自<random>的C++库,其中包含多个随机定义良好的引擎和各种整数和浮点类型的分布。


太棒了!那就是我在寻找的答案! - Uncertainly Certain
5
所有的伪随机数生成器(PRNGs)都没有“分布引擎”。分布引擎可以获取PRNG的原始随机值,并对这些值进行采样/转换以适应特定分布。如果您编写一个仿照C++ PRNG类型接口的包装器函数类,就可以使用任何C++随机分布函数来操作rand()函数。 - plasmacel
@plasmacel 非常正确。我主要是在考虑整个C随机库与整个C++11随机库,但这并没有写进文字中 : )。我重新表述了这篇文章。谢谢,非常好的观点。 - bolov
C语言的标准甚至没有规定rand函数生成的“伪随机数”必须遵循特定的分布,包括均匀分布。 - Peter O.

11

这里没有任何一个答案能够解释rand()为什么会不好的真正原因。

rand()是一个伪随机数生成器(PRNG),但这并不意味着它一定是差的。实际上,有非常好的PRNG,它们在统计学上很难或不可能与真正的随机数区分开来。

rand()完全是实现定义的,但历史上它被实现为一个线性同余生成器(LCG),通常是一类快速但声名狼藉的PRNG。这些生成器的低位比高位具有更低的统计随机性,生成的数字可以产生可见的晶格和/或平面结构(最好的例子是著名的RANDU PRNG)。一些实现尝试通过将位向右移一个预定义的量来减少低位问题,然而这种解决方案也会减少输出的范围。

尽管如此,还有一些优秀的LCG值得注意,例如L'Ecuyer在Tables of Linear Congruential Generators of Different Sizes and Good Lattice Structure, Pierre L'Ecuyer, 1999 中介绍的64位和128位乘法线性同余生成器。

通常的经验法则是:不要相信rand(),而是使用适合您需求和使用要求的自己的伪随机数生成器。


7
rand/srand有缺陷的地方在于rand
  • 使用未指定的算法生成数字序列,但是
  • 允许使用srand初始化该算法以实现可重复的“随机性”。
这两个点综合起来,阻碍了实现改进rand的能力(例如,使用加密随机数生成器[RNG]或用于产生伪随机数的其他“更好”的算法)。例如,JavaScript的Math.random和FreeBSD的arc4random没有这个问题,因为它们不允许应用程序为可重复的“随机性”提供种子。正是由于这个原因,V8 JavaScript引擎能够将其Math.random实现更改为xorshift128+的变体,同时保持向后兼容性。(另一方面,让应用程序提供额外数据以补充“随机性”,如BCryptGenRandom所示,问题就较少;即便如此,这通常只出现在加密RNG中。)
另外:
  • 算法和播种过程对于randsrand的规定未指定,这意味着即使在rand/srand实现之间(同一标准库的版本之间、操作系统之间等),可再现的“随机性”也不能保证。
  • 如果在调用rand之前没有调用srand,那么rand的行为类似于首先调用srand(1)。实际上,这意味着rand只能作为伪随机数生成器(PRNG)来实现,而不能作为非确定性RNG,并且无论应用程序是否调用srandrand的PRNG算法在给定的实现中都不能有所不同。

编辑(2020年7月8日):

randsrand的一个更加重要的问题在于它们在C标准中没有规定所谓的“伪随机数”必须遵循特定的分布,包括均匀分布或者近似均匀分布。相比之下,C++的uniform_int_distributionuniform_real_distribution类以及由C++指定的特定伪随机生成算法,如linear_congruential_enginemt19937,都有明确的分布规定。请注意,本文编辑于2020年12月12日。

关于randsrand,还有一个不好的地方:srand需要一个种子,该种子只能达到无符号整数的大小。 unsigned至少必须为16位,在大多数主流C实现中,unsigned根据实现的数据模型,可能是16位或32位(即使C实现采用64位数据模型,也不会是64位)。因此,通过这种方式最多只能选择2^N个不同的数字序列(其中N是unsigned中的位数),即使rand实现的底层算法可以生成更多不同的序列(例如,像C ++的mt19937一样可以产生2^128或甚至2^19937个序列)。


现在的 C 语言实现还是针对 32 位的吗? - heretoinfinity
为了回答这个问题,srand函数以单个unsigned作为其种子,并且unsigned的大小必须至少为16位,但通常为16或32位(即使在采用64位数据模型的C实现中也不是64位)。 - Peter O.
哇,最后一位是个惊喜。感谢更新。 - heretoinfinity

2
首先,srand()并不获取种子,而是设置种子。种子是任何伪随机数生成器(PRNG)使用的一部分。当给定种子后,PRNG从该种子产生的数字序列是严格确定性的,因为(大多数?)计算机没有生成真正随机数的手段。更改PRNG也不能阻止序列从种子开始重复出现,实际上这是有用的,因为能够产生相同的伪随机数序列通常很有用。
那么,如果所有PRNG都与rand()共享此特性,为什么rand()被认为不好呢?嗯,这要归结于“伪”这个词。我们知道PRNG不能真正地随机,但我们希望它的行为尽可能接近真正的随机数生成器,并且可以应用各种测试来检查PRNG序列与真正随机序列的相似程度。虽然标准未指定其实现方式,但在每个常用编译器中,rand()使用一种非常老的生成方法,适用于非常弱的硬件,并且其结果在这些测试上表现不佳。自此以后,已经创建了许多更好的随机数生成器,最好选择适合您需求的一种,而不是依赖于rand()提供的低质量随机数生成器。
适合您用途的生成器取决于您要做什么,例如您可能需要加密质量或多维度生成,但对于许多用途,您只需要相对均匀的随机性、快速生成,并且结果质量不影响事情的进行,那么您可能需要xoroshiro128+ 生成器。或者,您可以使用C++的<random>头文件中的方法,但所提供的生成器并非最先进,现在有更好的选择,但是它们对于大多数目的来说都足够好且非常方便。
如果涉及到钱财(例如在线赌场中的洗牌等)或者您需要加密质量,您需要仔细调查适当的生成器,并确保它们完全符合您的特定需求。

我的意思是使用srand来获取种子,而不是说它已经获取了种子。如果我没有表达清楚,对不起... - Uncertainly Certain
回复:“rand()使用了一种非常古老的生成方法”--没有这样的要求。 - Pete Becker
@PeteBecker:没有这样的要求,但所有常用的C++编译器都使用这种方法。标准是谈论C、C++或任何其他语言实际行为的愚蠢方式。 - Jack Aidley
@JackAidley -- 相反地:概括性的陈述(尤其是“所有常用的C++编译器...”)是谈论C、C++或任何其他语言行为方式的愚蠢方式。如果你想要做出准确的陈述,就要加上适当的限定条件。如果你说“我检查过的所有编译器的库(远远不是所有现有的编译器)都...”,那就是另一回事了(当然,前提是你确实进行了这样的调查或者可以以其他方式验证这样的说法)。 - Pete Becker
@Jack Aidley:你所说的设置种子的意思是一样的。 - Uncertainly Certain

1

rand通常是一个非常糟糕的伪随机数生成器(PRNG),但并非总是如此,这是由其实现方式决定的。

C++11有很好的、更好的PRNG。使用其<random>标准头文件。特别是看看这里std::uniform_int_distribution,其中有一个很好的示例{{link4:std::mersenne_twister_engine}}。

PRNG是一个非常棘手的问题。我对它们一无所知,但我相信专家们。


0

让我再给你添加一个使rand()完全无法使用的理由:标准没有定义它生成的随机数的任何特性,包括分布和范围。

没有分布的定义,我们甚至无法将其封装为想要的分布。

更进一步地,理论上我可以通过简单地返回0来实现rand()函数,并宣称我的rand()的RAND_MAX是0。

甚至更糟糕的是,我可以让最低有效位始终为0,这不违反标准。想象一下有人写了这样的代码:if (rand()%2) ...

实际上,rand()的实现是由编译器定义的,标准也如下所述:

不能保证生成的随机序列的质量,某些实现已知会产生具有非常低位非随机性的序列。对于需要特定要求的应用程序,应该使用已知能满足其需求的生成器。

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf p36


-4
如果您使用rand(),那么在生成随机数后,基本上会得到相同的结果。因此,即使使用srand(),如果有人能够猜测您使用的种子,那么预测生成的数字将变得容易。这是因为函数rand()使用特定算法来产生这些数字。
通过浪费一些时间,您可以找出如何预测由该函数生成的数字,只要给出种子即可。现在您所需要的就是猜测种子。有些人将种子称为当前时间。因此,如果能够猜测您运行应用程序的时间,我就能够预测数字。
使用rand()是不好的!!!

3
算法的实现取决于具体情况。请参考 https://dev59.com/PHNA5IYBdhLWcg3wSrqa。 - VLL
您所指定的问题只有在生成器具有特定需求时才是问题。这不是rand()的一般问题。 - Jack Aidley
4
每个伪随机数生成器都使用特定算法来生成其结果。能否预测下一个数字取决于算法的细节。C和C++都不要求rand()被实现得糟糕。 - Pete Becker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接