为什么这个随机数生成器生成相同的数字?

问题描述 投票:2回答:4

第一个工作,但第二个总是返回相同的值。为什么会发生这种情况,我该如何解决这个问题?

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_real_distribution<> dis(0, 1);

    for(int i = 0; i < 10; i++) {

        std::cout << dis(gen) << std::endl;
    }return 0;
}

一个不起作用:

double generateRandomNumber() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_real_distribution<> dis(0, 1);

    return dis(gen);
}



int main() {
    for(int i = 0; i < 10; i++) {
        std::cout << generateRandomNumber() << std::endl;
    }return 0;
}
c++ random
4个回答
5
投票

你在做什么平台?如果不存在生成随机数的硬件或OS功能,则允许std::random_device为伪RNG。它可能会使用当前时间进行初始化,在这种情况下,您调用它的时间间隔可能会因“当前时间”而过于接近另一个值。

然而,正如评论中所提到的,它并不意味着以这种方式使用。一个简单的解决方法是将rdgen声明为static。正确的解决方法是将RNG的初始化移出需要随机数的函数,因此它也可以被需要随机数的其他函数使用。


3
投票

第一个对所有数字使用相同的生成器,第二个为每个数字创建一个新的生成器。


2
投票

请注意,std::mt19937 gen(rd())非常有问题。见this question,其中说:

  • rd()返回单个unsigned int。这至少有16位,可能是32位。这还不足以播种[这个发电机的巨大状态]。
  • 使用std::mt19937 gen(rd());gen()(以32位播种并查看第一个输出)不能提供良好的输出分布。 7和13永远不会是第一个输出。两粒种子产生0.十二粒种子产生1226181350.(Link
  • std::random_device可以(有时是)实现为具有固定种子的简单PRNG。因此,它可能在每次运行时产生相同的序列。 (Link

此外,random_device生成“非确定性”随机数的方法是“实现定义的”,并且random_device允许实现“使用随机数引擎”,如果由于“实现限制”而无法生成“非确定性”随机数([ rand.device])。 (例如,在C ++标准下,实现可能使用系统时钟的时间戳或使用快速移动的循环计数器来实现random_device,因为两者都是不确定的。)

应用程序不应盲目地调用random_device的生成器(rd()),至少不要调用entropy()方法,该方法以位为单位估算实现的熵。


1
投票

让我们比较两个案例之间的差异,看看为什么会发生这种情况。


情况1:

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_real_distribution<> dis(0, 1);

    for(int i = 0; i < 10; i++) {    
        std::cout << dis(gen) << std::endl;
    }return 0;
}

在你的第一种情况下,程序执行main函数,这里发生的第一件事是你在堆栈上创建一个std::random_devicestd::mt19337std::uniform_real_distribution<>的实例,属于main()的范围。你的mersenne twister gen用随机设备rd的结果初始化一次。您还初始化了您的分布dis,以获得从01的值范围。这些仅在每次运行应用程序时存在一次。

现在你创建一个从索引0开始并递增到9的for循环,并且在每次迭代中,你通过使用分布coutdis将结果值显示给operator()(),传递给你已经播种的一代gen。每次在这个循环中,dis(gen)将产生不同的值,因为gen已经只播种了一次。


案例2:

double generateRandomNumber() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_real_distribution<> dis(0, 1);    
    return dis(gen);
}

int main() {
    for(int i = 0; i < 10; i++) {
        std::cout << generateRandomNumber() << std::endl;
    }return 0;
}

在这个版本的代码中,让我们看看它们的相似之处和不同之处。这里程序执行并进入main()函数。这次遇到的第一件事是从09的for循环,类似于上面的主要部分,但是这个循环是main的堆栈上的第一件事。然后调用cout来显示来自user defined function的名为generateRandomNumber()的结果。这个函数被称为总共10次,每次迭代for循环时,这个函数都有自己的堆栈内存,它将被缠绕和展开,或者被创建和销毁。

现在让我们跳进这个名为user defined functiongenerateRandomNumber()

代码看起来几乎与之前直接在main()中的代码完全相同,但这些变量存在于generateRandomNumber()的堆栈中,并且具有其范围的生命周期。每次此函数进入和超出范围时,都将创建和销毁这些变量。这里的另一个区别是这个函数也返回dis(gen)

注意:我不是100%确定这是否会返回copy或者编译器最终会进行某种优化,但是按值返回通常会产生副本。

最后,当函数generateRandomNumber()返回并且恰好在它完全超出范围之外,其中std::uniform_real_distribrution<>operator()()被调用并且它进入它自己的堆栈和范围,然后再返回主generateRandomNumber(),然后再回到main。


- 可视化差异 -

正如你所看到的,这两个程序是完全不同的,确切地说是非常不同的。如果你想要更多的视觉证明它们是不同的,你可以使用任何可用的在线编译器输入每个程序,它在assembly中显示你的程序,并比较两个组件版本,看看它们的最终差异。

可视化这两个程序之间的差异的另一种方法不仅是看到他们的assembly等价物,而是逐步通过每个程序与debugger,并密切关注stack calls和它们的缠绕和展开,并留意所有值被初始化,返回和销毁。


- 评估和推理 -

第一个按预期工作的原因是因为你的random device,你的generator和你的distribution都有main的生命时间,你的generator只用你的随机设备播种一次,你只有一个你每次使用的分布for循环。

在你的第二个版本中,main并不知道任何关于它的任何内容,它知道的是它正在通过for循环并将返回的数据从用户函数发送到cout。现在,每次进入for循环时,都会调用此函数,并且正如我所说的那样,它是每次创建和销毁的堆栈,所以如果它的变量都被创建和销毁的话。因此,在这种情况下,您正在创建和销毁10: rdgen(rd())dis(0,1)s实例。


-结论-

除了我上面描述的内容以及与随机数生成器的行为有关的另一部分之外,还有用户Kane在他对你的评论中给你的声明中提到的内容:

来自en.cppreference.com/w/cpp/numeric/random/random_device:“std :: random_device可以用实现定义的伪随机数引擎实现[...]。在这种情况下,每个std :: random_device对象可以生成相同的数字序列。”

每次你创建和销毁你都会用一个新的generator一遍又一遍地播种random_device但是如果你的特定机器或操作系统不支持使用random_device它可能最终使用一些任意值作为其种子值,或者它可以最终使用系统时钟生成种子值。

因此,假设它最终使用系统时钟,main() for for循环的执行发生得如此之快,以至于10调用generateRandomNumber()所做的所有工作在几毫秒过去之前就已经执行了。所以这里的delta时间是最小的,可以忽略不计,因为它在每次传递时生成相同的种子值,并且它从分布中生成相同的值。