【问题标题】:Why is this random number generator generating same numbers?为什么这个随机数生成器会生成相同的数字?
【发布时间】:2018-11-07 08:21:53
【问题描述】:

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

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;
}

【问题讨论】:

  • std::mt19937 gen(rd()); 在你的程序中只应该被调用一次。
  • 来自en.cppreference.com/w/cpp/numeric/random/random_device:“std::random_device 可以根据实现定义的伪随机数引擎来实现 [...]。在这种情况下,每个 std::random_device 对象都可以生成相同的数列。”

标签: c++ random


【解决方案1】:

您在哪个平台上工作?如果不存在生成随机数的硬件或操作系统功能,则std::random_device 被允许为伪 RNG。它可能会使用当前时间进行初始化,在这种情况下,您调用它的时间间隔可能太近,以至于“当前时间”无法采用另一个值。

尽管如此,如 cmets 中所述,它并不打算以这种方式使用。一个简单的解决方法是将rdgen 声明为static。一个适当的解决方法是将 RNG 的初始化从需要随机数的函数中移出,这样它也可以被其他需要随机数的函数使用。

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • 更准确地说:它创建的新生成器都具有相同的种子值。
    【解决方案3】:

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


    案例 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 函数,这里发生的第一件事是您在属于 main() 的堆栈上创建 std::random_devicestd::mt19337std::uniform_real_distribution&lt;&gt; 的实例的范围。您的 mersenne twister gen 会使用随机设备 rd 的结果初始化一次。您还初始化了您的分布dis,使其具有从01 的值范围。每次运行您的应用程序时,这些仅存在一次。

    现在您创建一个从索引0 开始并递增到9 的for 循环,并且在每次迭代中,您通过使用分布dis 传递给的operator()() 将结果值显示到cout它是你已经播种的一代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,但是这个循环是main 堆栈上的第一件事。然后调用cout 以显示来自名为generateRandomNumber()user defined function 的结果。这个函数总共被调用了10 次,每次你遍历 for 循环时,这个函数都有自己的堆栈内存,将被缠绕和解开或创建和销毁。

    现在让我们跳到这个名为generateRandomNumber()user defined function

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

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

    最后,当 then 函数 generateRandomNumber() 返回时,就在它完全超出调用 std::uniform_real_distribrution&lt;&gt;operator()() 的范围之前,它进入自己的堆栈和范围,然后返回到主 generateRandomNumber()这么简单,然后回到主要的。


    -可视化差异-

    正如您所见,这两个程序完全不同,确切地说是非常不同。如果您想更直观地证明它们不同,您可以使用任何可用的在线编译器将每个程序输入到它在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 对象都可能生成 相同的数列。”

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

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

    【讨论】:

      【解决方案4】:

      注意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() 方法,该方法以位为单位估计实现的熵。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-12-31
        • 1970-01-01
        • 1970-01-01
        • 2023-03-30
        • 1970-01-01
        • 2011-05-21
        • 1970-01-01
        相关资源
        最近更新 更多