【问题标题】:Random numbers for multiple threads多个线程的随机数
【发布时间】:2013-02-02 03:38:28
【问题描述】:

问题

我打算为 Linux 编写一个 C++11 应用程序,它基于大约一百万个伪随机 32 位数字进行一些数值模拟(不是密码学)。为了加快速度,我想使用桌面 CPU 的所有内核在并行线程中执行模拟。我想使用 boost 提供的 Mersenne Twister mt19937 作为 PRNG,我想出于性能原因,每个线程我应该有一个这样的 PRNG。现在我不确定如何播种它们以避免在多个线程中生成相同的随机数子序列。

替代方案

以下是我目前想到的替代方案:

  1. 独立于/dev/urandom 为每个线程播种 PRNG。

    我有点担心系统熵池耗尽的情况,因为我不知道系统内部 PRNG 是如何运行的。由于/dev/urandom 正在使用 Mersenne Twister 本身这一事实,我是否会意外地获得准确识别 Mersenne Twister 连续状态的连续种子?可能与我对下一点的担忧密切相关。

  2. /dev/urandom 播种一个 PRNG,从第一个播种其他 PRNG。

    基本上也是同样的问题:使用一个 PRNG 来播种另一个使用相同算法的 PRNG 是好还是坏?或者换句话说,从mt19937 读取 625 个 32 位整数是否直接对应于 mt19937 生成器在这一代期间的任何时候的内部状态?

  3. 从一开始就用非梅森信息播种其他人。

    由于使用相同的算法来生成随机数和生成初始种子在某种程度上感觉可能是个坏主意,因此我考虑引入一些不依赖于 Mersenne Twister 算法的元素。例如,我可以将线程 id 异或到初始种子向量的每个元素中。这会让事情变得更好吗?

  4. 在线程之间共享一个 PRNG。

    这将确保只有一个序列具有 Mersenne Twister 的所有已知和理想特性。但是控制对该生成器的访问所需的锁定开销确实让我有些担心。由于我没有发现相反的证据,我假设我作为图书馆用户将负责防止对 PRNG 的并发访问。

  5. 预生成所有随机数。

    这将使一个线程预先生成所有需要的 1M 随机数,供以后不同的线程使用。与整个应用程序相比,4M 的内存需求会很小。这种方法最让我担心的是随机数的生成本身并不是并发的。整个方法也不能很好地扩展。

问题

您会建议其中哪些方法,为什么?或者您有什么不同的建议?

您知道我的哪些担忧是有道理的,哪些仅仅是因为我对事情的实际运作缺乏洞察力吗?

【问题讨论】:

  • 我之前也有同样的问题。 stackoverflow.com/questions/14804808/… 幸运的是我在使用 Java
  • @YankeeWhiskey,accepted answer there 在这里看起来像选项 3:您从由 SecureRandom 生成的 UUID 播种它们,而 SecureRandom 又使用依赖于平台的熵源,而不仅仅是梅森捻线机。
  • 所有建议的方法都将导致生成重复的随机数。通常,您从可能的 2**32 个数字中要求 2*20 个“随机”数字。这要求很多,因此您需要重新考虑您想要从 100 万个随机 32 位整数中获得哪些属性。如果唯一性是其中之一,那么这些方法都不起作用。
  • @GregS,个别重复的数字不会让我担心。我可能应该指定子序列长度的下限。我想说由两个线程精确复制的 10 个数字序列可能开始给我带来麻烦。但是 2**320 位的偶然巧合似乎不太可能,以至于我认为一旦两个线程有​​这么多共同的数字,它们可能也会有更多的共同点。
  • 嗯,听起来您已经考虑过了,这很好。我担心的是生日悖论的结果。只要少数重复对你的算法来说不是致命的,你应该没问题。

标签: c++ multithreading boost random mersenne-twister


【解决方案1】:

我会选择 #1,从 urandom 播种每个 prng。这确保了状态是完全独立的(只要种子数据是独立的)。除非你有很多线程,否则通常会有很多可用的熵。此外,根据用于 /dev/urandom 的算法,您几乎可以肯定不需要担心它。

所以你可以使用类似下面的东西来创建每个 prng:

#include <random>

std::mt19937 get_prng() {
    std::random_device r;
    std::seed_seq seed{r(), r(), r(), r(), r(), r(), r(), r()};
    return std::mt19937(seed);
}

您应该验证您的std::random_device 实现是从您的配置下的/dev/urandom 中提取的。如果它默认使用 /dev/urandom,那么如果你想改用 /dev/random,通常你可以说std::random_device("/dev/random")

【讨论】:

  • 不仅感谢您对如何选择的意见,还感谢您指出我从 boost (mt19937) 导入或自己实现的大部分内容 (random_device) 在 C+ 中标准化+11,即使 API 有所不同。可能有助于避免对 boost 的依赖。
  • 提醒我,当我第一次检查时(我认为是几年前),各种编译器没有使用相同的 mt19937 实现(相同的种子有不同的结果),因此使用 boost 可以更好地重现性。不知道现在怎么样了。
  • @EamonNerbonne 引擎需要产生相同的结果。然而,分布不是。
  • 你是完全正确的:我刚刚检查过,确实 MSC 和 GCC 例如从原始 mt19937 生成器的相同种子返回相同的序列,但使用该生成器进行均匀分布的结果不同(我也会打赌其他发行版)。但是,这仍然存在潜在问题:如果您想在某些发行版下重现 RNG 序列,则不能使用内置的 c++11 库。
  • (顺便提一下,MSC 还不支持return std::mt19937{q}; 构造语法)。
【解决方案2】:

您可以使用具有不同代数结构的 PRNG 来播种不同的 PRNG。例如。一些 MD5 哈希序列。

但是我会选择#5。如果它有效,那很好。如果没有,您仍然可以优化它。

关键是创建一个好的 PRNG 比人们想象的要困难得多。对于线程应用程序来说,一个好的 PRNG 很可能还有待研究。

如果 CPU 的数量足够少,您就可以摆脱跳跃式蛙泳。例如。如果您有 4 个核心,请使用相同的值初始化所有核心,但将核心 1 PRNG 提前 1、#2 和 #3 提前 3。然后在需要新数字时始终提前 4 步。

【讨论】:

    【解决方案3】:

    我会使用一个实例来播种其他实例。我很确定您可以相当轻松地安全地做到这一点。

    • 即使状态空间中的微小变化也会导致下游相当大的变化 - 如果您可以确保它们没有完全相同的起始空间(并且没有相同的状态前缀),我不会担心产生相同的数字。例如,仅使用值 1,2,3 为三个线程播种就可以正常工作 - 您甚至不需要为整个空间播种。另一个优势:通过使用清晰可预测的种子,您可以轻松地抹黑您正在挑选任何运行的想法(假设您正在尝试展示某些东西)。
    • 以某种方式播种是微不足道的,这意味着由此产生的“孩子”是高度不相关的。只需以广度优先的方式进行迭代;即,如果你想播种 N x 623 个 int 值,不要按顺序播种 623 个值,而是选择第一个 N 并分发,然后是下一个 N 等等。即使播种机和孩子之间存在一些相关性,各种各样的孩子应该几乎不存在 - 这就是你所关心的。
    • 我更喜欢尽可能允许确定性执行的算法,因此依赖 urandom 没有吸引力。这使调试更容易。
    • 最后,很明显 - 测试。这些 PRNG 相当强大,但无论如何都要关注结果并根据您正在模拟的内容进行一些相关性测试。大多数问题应该是显而易见的——要么你播种不好并且有明显的重复子序列,要么你播种好,然后质量取决于 PRNG 的限制。
    • 对于最终执行,在完成测试后,您可以使用 urandom 和/或线程 ID 为 623 个状态值中的第一个播种。

    【讨论】:

    • 并行播种在行为方面听起来非常有趣。实施它可能很麻烦,因为我不能简单地将一个 PRNG 作为种子传递给所有其他人。但我想我可以简单地预先生成 8*623 字节,转置该矩阵并将结果数组传递给构造函数或种子函数。或者只是按照您的建议使用一个整数作为种子。关于调试的观点也很有效。
    • 是的,转置就可以了。或者只使用 2 个嵌套循环 - 您实际上不需要并行执行此操作,因为一旦完成,您总是可以在之后交出 PRNG。
    • 我没有考虑并行进行初始化。但是使用 boost 的播种步骤似乎是一个原子操作。我不能直接播种个人价值观。所以我必须找到一种方法来为单次调用提供一个完整的状态向量。
    • 当然,由于 API 限制,您需要在传递 em 之前收集值 - 但这不是一个严重的减速带,对吧?
    • 不,一点也不,但这意味着嵌套循环不适用于播种。它们将用于生成(已经转置的)矩阵。无论如何,这只会使实现比我希望的要长一些,但应该可以很好地工作。
    【解决方案4】:

    种子线程 1 与 1,种子线程 2 与 2,等等。

    如果您需要蒙特卡罗,这将为您提供可重复的结果,易于跟踪和实施。

    【讨论】:

    • 这是一个相当不错且极其简单的解决方案。
    【解决方案5】:

    请看以下论文:Dynamic Creation of Pseudorandom Number Generators 和随附的实现:Dynamic Creator。它解决了这个确切的问题。

    【讨论】:

    • 听起来不错,不过在我真正读完那个野兽之前,我会拒绝投票。
    • 那些人肯定知道他们在说什么,因为 Mersenne Twister 也是基于他们的工作。谢谢指点!使用他们的代码是一种可能性,而使用他们的代码来静态计算mersenne_twister_engine 的一堆(即预期的核心数量)的参数是另一种可能性。
    【解决方案6】:

    如果您真的想在数学上正确,请使用 SFMT 算法作者提供的跳转函数。跳转函数保证两个不同 PRNG 流之间的最小序列数。

    然而,实际上,/dev/urandom 初始化就足够了。

    【讨论】:

    【解决方案7】:

    我会说#3 是赢家。使用 processID 或 threadID 之类的东西为每个线程播种;虽然从技术上讲你可能会重叠,但这种可能性极小。一旦你离开个位数,即使是连续的数字也不应该与种子相关(我不知道 Twister 算法,但我见过的最差的 PRNG 高于 7)。与大多数 PRNG 方程的范围相比,一百万个 PRNG 并不算多。

    最后,您可以相当轻松地进行检查。检查每个线程生成的 last 种子与每个其他线程中的所有数字。如果种子出现在线程中,则检查之前在每个线程中生成的数字;如果它们也匹配,那么您就发生了冲突,需要重新播种您的流并重试。

    【讨论】:

      【解决方案8】:

      有一个实现(和已发表的论文)专门涉及使用 Mersenne Twister 进行并行计算。它是由 MT 的原始作者编写的。他们将其称为“Dynamic Creator”,可以在这里找到:

      http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/DC/dc.html

      那将是研究您对 MT19937 的具体用法的好地方,尤其是那里的论文。

      【讨论】:

      • answer by NPE 提供了几乎相同的信息,尽管它没有指出这些是 MT 的原始作者。
      猜你喜欢
      • 1970-01-01
      • 2023-03-28
      • 2016-05-15
      • 1970-01-01
      • 2023-04-04
      • 1970-01-01
      • 2012-01-09
      • 2016-05-13
      • 1970-01-01
      相关资源
      最近更新 更多