`rand()` 的用处 - 或者谁应该调用 `srand()`?
Posted
技术标签:
【中文标题】`rand()` 的用处 - 或者谁应该调用 `srand()`?【英文标题】:Usefulness of `rand()` - or who should call `srand()`? 【发布时间】:2014-12-05 07:30:34 【问题描述】:背景:我在代码中使用rand()
、std::rand()
、std::random_shuffle()
等函数进行科学计算。为了能够重现我的结果,我总是明确指定随机种子,并通过srand()
设置它。直到最近,当我发现 libxml2 也会在第一次使用时懒惰地调用 srand()
- 这是在我早期的 srand()
调用之后。
我填写了bug report to libxml2 about its srand()
call,但我得到了答案:
然后先初始化 libxml2。 这是从图书馆发出的完全合法的电话。你应该 不要指望没有其他人打电话给
srand()
,并且手册页无处可去 声明应避免多次使用srand()
这实际上是我现在的问题。如果一般政策是每个库都可以/应该/将调用srand()
,并且我可以/可能还会在这里和那里调用它,我真的不知道这有什么用处。或者rand()
有什么用处?
这就是为什么我认为,一般(不成文的)策略是任何库都不应该调用srand()
,并且应用程序应该在开始时只调用一次。 (不考虑多线程。我想在这种情况下,无论如何你应该使用不同的东西。)
我还尝试研究一下哪些其他库实际上调用了srand()
,但我没有找到。有吗?
我目前的解决方法是这个丑陋的代码:
// On the first call to xmlDictCreate,
// libxml2 will initialize some internal randomize system,
// which calls srand(time(NULL)).
// So, do that first call here now, so that we can use our
// own random seed.
xmlDictPtr p = xmlDictCreate();
xmlDictFree(p);
srand(my_own_seed);
可能唯一干净的解决方案是根本不使用它,而只使用我自己的随机生成器(可能通过C++11 <random>
)。但这不是真正的问题。 问题是,谁应该打电话给srand()
,如果每个人都打电话,那么rand()
有什么用呢?
【问题讨论】:
如果你可以使用 C++11 库,你应该看看en.cppreference.com/w/cpp/numeric/random IMO 这是你的设计缺陷。当您需要可预测的随机值(addictio in adiectio 本身)时,您永远不应该使用程序的所有其他部分(包括您使用的库)也可能使用的全局随机生成器。即使那些其他部分不调用srand()
,只要调用rand()
,它们也会影响全局随机生成器。
对不起,出于安全原因使用 srand/rand,正如 libxml2 家伙声称的那样,是可笑的。
修改全局状态的库(包括调用srand()
)已损坏。他们的回答只是胡扯,试图为不合理的事情辩护。其余的:如果您需要可重复的随机数进行科学计算,rand()
不会满足您的要求。游戏很好,或者只是玩玩,但仅此而已。否则,在 C++11 之前的版本中,您实现自己的,而在 C++11 中,您使用 <random>
。 (但这并不能让库实现者摆脱困境。)
@JamesKanze “修改全局状态的库(包括调用 srand())已损坏。” -- 调用 rand 修改全局状态,就像 srand 一样。被破坏的是这些函数具有全局状态。
【参考方案1】:
改用新的<random>
标头。它允许多个引擎实例,使用不同的算法,更重要的是,为您提供独立的种子。
[编辑]
为了回答“有用”的部分,rand
生成 随机 数字。这就是它的好处。如果您需要细粒度的控制,包括可重复性,您不仅应该有一个已知的种子,还应该有一个已知的算法。 srand
最多只能给你一个固定的种子,所以这不是一个完整的解决方案。
【讨论】:
@Albert:您的字面问题的答案是“它没有用,至少从可测试性/可重复性的角度来看不是”。唯一的办法就是封装你自己的RNG状态;新的 C++ 库会为您做到这一点。 回复:他们的回应,因为srand
既不是线程安全的,也不是可重入的,多次从不同的线程调用它根本不安全(rand
有类似的问题)。这里有一个真正的基本问题,通过 C++11 中的多个生成器的可能性得到解决。
@Albert 你的问题很像“世界不应该和平吗?” ...无论答案如何,都没有。 libxml2 调用srand
是事实。继续前进,按照这个答案的建议去做。
@Albert 这是一个毫无意义的哲学练习。维护者不会因为这里的讨论而改变它。这当然不是一个错误,但它似乎对你来说是个问题......所以再次,做其他事情。
@MSalters:许多实现记录了 2^32 的周期,如果符合其可移植性目标,代码依赖实现定义的行为是合理的。 “此外,libXML 获得与您相同的随机值并不重要,因为它们也使用相同的种子调用 srand()。” - 这是对 rand()
工作方式的误解 - 它不会给 libXML 与应用程序相同的序列,因为它们共享状态,问题是如果说应用程序是随机数的严重消费者,那么 libXML 的 srand()
调用可以重新启动[sub]对已经生成的应用进行排序。【参考方案2】:
好吧,显而易见的事情已经被其他人说过几次了,使用新的 C++11 生成器。不过,我出于不同的原因重述它。
您将输出用于科学计算,而rand
通常实现了一个相当差的生成器(同时,许多主流实现使用 MT19937,除了糟糕的状态恢复还不错,但你无法保证特定算法,并且至少有一个主流编译器仍然使用非常差的 LCG)。
不要用糟糕的生成器进行科学计算。如果你在手机上玩一些射击小鸟的愚蠢游戏,你的随机数中是否有超平面之类的东西并不重要,但对于科学模拟来说,这很重要。永远不要使用坏的发电机。不要。
重要提示:std::random_shuffle
(带有两个参数的版本)实际上可能会调用rand
,如果您正在使用它,即使您使用新的 C++11,也要注意这是一个陷阱在<random>
中找到的生成器。
关于实际问题,调用srand
两次(甚至更频繁)是没有问题的。原则上你可以随意调用它,它所做的只是改变种子,随之而来的伪随机序列。我想知道为什么 XML 库会想要调用它,但他们的回应是正确的,他们这样做并不是非法的。但这也没关系。
唯一需要确保的是 either 您不关心获得任何 特定 伪随机序列(也就是说,任何序列都可以,您不感兴趣复制一个精确的序列),或你是最后一个调用srand
的人,这将覆盖之前的所有调用。
也就是说,用 3 到 5 行代码实现您自己的具有良好统计属性和足够长周期的生成器也不是那么难,只需小心一点。主要优势(除了速度)是您可以准确控制您的状态在哪里以及由谁修改它。 您不太可能需要比 2128 长得多的时间段,因为实际消耗这么多数字的时间太长了。 每个周期消耗一个数字的 3GHz 计算机将在 2128 周期上运行 1021 年,因此对人类来说没有太大问题平均寿命。即使假设您运行模拟的超级计算机速度快一万亿倍,您的孙子孙女也不会活到这一时期结束。 就目前而言,像 219937 这样当前“最先进的”生成器提供的时期真的很荒谬,如果你问我,那就是试图在错误的一端改进生成器(最好确保它们是统计上是坚定的,并且他们从最坏的情况下迅速恢复,等等)。当然,这里的意见可能会有所不同。
This site 列出了几个带有实现的快速生成器。它们是 xorshift 生成器,结合了一个加法或乘法步骤和一个小的(从 2 到 64 个机器字)滞后,这导致了快速和高质量的生成器(还有一个测试套件,该网站的作者写了几个有关该主题的论文)。我正在使用其中之一的修改(移植到 64 位的 2 字 128 位版本,相应地修改了移位三元组)。
【讨论】:
【参考方案3】:这个问题正在 C++11 的随机数生成中得到解决,即你可以创建一个类的实例:
std::default_random_engine e1
它允许您仅完全控制从对象e1
生成的随机数(与 libxml 中使用的任何内容相反)。一般的经验法则是使用新结构,因为您可以独立生成随机数。
Very good documentation
为了解决您的问题 - 我还认为在 libxml 之类的库中调用 srand()
是一种不好的做法。然而,更多的是srand()
和rand()
并非设计用于您尝试使用它们的上下文 - 当您只需要一些随机数时它们就足够了,就像 libxml 一样。但是,当您需要可重复性并确保您独立于其他人时,新的 <random>
标头是您的最佳选择。所以,总而言之,我认为图书馆方面不是一个好的做法,但很难责怪他们这样做。此外,我无法想象他们会改变这一点,因为数十亿其他软件可能依赖于它。
【讨论】:
【参考方案4】:真正的答案是,如果您想确保您的随机数序列不会被其他人的代码更改,您需要一个对您的工作来说是私有的随机数上下文。请注意,调用srand
只是其中的一小部分。例如,如果您在调用rand
的其他库中调用某个函数,它也会破坏您的随机数序列。
换句话说,如果您希望代码具有可预测的行为,基于随机数生成,它需要与任何其他使用随机数的代码完全分开。
其他人建议使用 C++ 11 随机数生成,这是一种解决方案。
在 Linux 和其他兼容的库上,您还可以使用 rand_r
,它将指向 unsigned int
的指针指向用于该序列的种子。因此,如果您初始化 seed
变量,然后将其与对 rand_r
的所有调用一起使用,它将为您的代码生成一个唯一序列。这当然仍然是旧的rand
生成器,只是一个单独的种子。我这么说的主要原因是你可以很容易地做这样的事情:
int myrand()
static unsigned int myseed = ... some initialization of your choice ...;
return rand_r(&myseed);
只需调用myrand
而不是std::rand
(并且应该可以使用带有随机生成器参数的std::random_shuffle
)
【讨论】:
是的!所以对。仅仅将srand()
调用移出libxml 库并不会真正使您的结果非常可重复。它可能会起作用,但显然某些 libxml 函数在某些时候调用 rand()
(计算 UUID?),这将改变您的程序在此之后从 rand()
接收的伪随机序列......以上是关于`rand()` 的用处 - 或者谁应该调用 `srand()`?的主要内容,如果未能解决你的问题,请参考以下文章