从<随机>重复重新播种 PRNG 是不可重现的

Repeated reseeding of a PRNG from <random> isn't reproducible

提问人:kaisong 提问时间:9/1/2023 最后编辑:Peter Mortensenkaisong 更新时间:9/5/2023 访问量:121

问:

C++中有确定性随机数生成器吗?<random>

有问题的一点是,我的 Windows 机器上的以下代码:

#include<iostream>
#include<random>
int main(){
  std::mt19937 g;
  std::normal_distribution<double> d;
  for(int i=0;i<100;++i){
    g.seed(65472381);
    std::cout << "List[65472381] = " << d(g) << "\n";
  }
}

产生以下结果:

List[65472381]=0.972683
List[65472381]=-0.773812
List[65472381]=0.972683
List[65472381]=-0.773812
List[65472381]=0.972683
List[65472381]=-0.773812
...

我的困惑在于,尽管它之前每次都会重置为种子。0.972683 != -0.77381265472381g

我的处理器是 Zen 2,操作系统是 Windows 10 Pro 版本 22H2。编译器是 GCC (x86_64-w64-mingw32/12.2.0)。但是,通过在不同的虚拟机和编译器上在线测试代码,结果似乎在您的计算机上也可能是相同的。

实际上寻求的是一种从时空 O(1) 中随机分布数的任意固定通用列表中获取第 i 个数的方法,该列表长度为 4,294,967,295,这意味着该列表的任何元素都不会被存储。

C++ 随机种子 MT19937

评论


答:

3赞 Yksisarvinen 9/2/2023 #1

每个生成器(除了可能的 std::random_device)在C++上都是确定性的。

但是,发行版可以(并且确实)保持状态在我的编译器上,调用生成器 4 次并使用它来生成 2 个数字。对于以下代码:std::normal_distribution

#include <iostream>
#include <random>

std::mt19937 g;

struct MyGenerator {
    auto operator()() {
        std::cout << "I'm called!\n";
        return g();
    }
    static auto max() { return g.max(); }
    static auto min() { return g.min(); }
};

int main() {
    std::normal_distribution<double> d;
    MyGenerator gen;
    g.seed(65472381);
    std::cout << "List[0] = " << d(gen) << "\n";
    g.seed(65472381);
    std::cout << "List[1] = " << d(gen) << "\n";
    g.seed(65472381);
    std::cout << "List[2] = " << d(gen) << "\n";
}

我得到以下输出:

List[0] = I'm called!
I'm called!
I'm called!
I'm called!
0.972683
List[1] = -0.773812
List[2] = I'm called!
I'm called!
I'm called!
I'm called!
0.972683

这解释了您看到的输出。每一秒调用 不会请求任何新的熵,而是依赖于先前生成的熵。d(g)g

您可以在分布对象上调用 reset() 方法来重置它所持有的任何内部状态,并确保在生成器具有相同值的种子时获得一致的结果。


在 O(1) 时间内(可能)不可能获得第 i 个数。您可以在引擎上调用 discard() 方法来跳过它生成的一些熵,但它有 2 个问题:std::mt19937

  • 不能保证它会比循环快(可能是也可能不是,不能保证)
  • 你实际上不知道你的发行版需要多少熵,所以你不知道要丢弃多少才能达到它产生的第i个数,这都是你的标准库实现的内部行为,可以随时改变

评论

0赞 kaisong 9/2/2023
感谢您提供更详细和更富有启发性的解释。我检查了tbxfreeware的答案作为解决方案,因为它最直接地解决了面临的问题。
12赞 tbxfreeware 9/2/2023 #2

分发对象具有内部状态。因此,在重新设定随机数引擎的种子后,必须重置分布以清除其内部状态。对于标准库中的所有引擎和发行版都是如此。通常,在为引擎播种时,还应该重置分配。

#include<iostream>
#include<random>

int main() {
    std::mt19937 g;
    std::normal_distribution<double> d;
    for (int i = 0; i < 10; ++i) {
        g.seed(65472381);
        d.reset();
        std::cout << "List[65472381] = " << d(g) << "\n";
    }
}

输出如下:

List[65472381] = -0.773812
List[65472381] = -0.773812
List[65472381] = -0.773812
List[65472381] = -0.773812
List[65472381] = -0.773812
List[65472381] = -0.773812
List[65472381] = -0.773812
List[65472381] = -0.773812
List[65472381] = -0.773812
List[65472381] = -0.773812
Mersenne Twister的高效功能?discard

哪些 C++ 随机数引擎具有 O(1) 丢弃函数?

这个 Stack Overflow 问题的答案解释了 Mersenne Twister 有一个“快速跳跃”算法。这意味着高效的功能是可能的。不幸的是,C++ 标准并没有强制要求实现使用它。discard

您可能会发现您的系统是一个具有高效 .但是,您不能假设每个系统都会这样做。discard

如果您决定使用 ,请务必在之后重置分配。否则,不能保证分布生成的值是可重复的。discard

是便携的吗?std::mt19937

正如我在下面的评论中指出的那样,C++标准要求可移植。它必须在每个实现中生成相同的随机数序列。std::mt19937

我使用以下程序从 生成 10 个值。我使用最新版本的 MSVC 运行它。如果在系统上运行它,则应获得相同的输出。std::mt19937

#include<iostream>
#include<random>
int main() {
    std::mt19937 g;
    unsigned seed{ 65472381u };
    g.seed(seed);
    std::cout << "Seed : " << seed << "\n\n";
    for (int i = 0; i < 10; ++i) {
        std::cout << "g() : " << g() << "\n";
    }
}

这是输出。

Seed : 65472381

g() : 3522518833
g() : 1238868101
g() : 1353561095
g() : 3289615924
g() : 1455032182
g() : 573730142
g() : 700682001
g() : 2371867773
g() : 3721872455
g() : 2742745620
随机数分布不可移植

无论好坏,C++ 标准并不要求每个实现中的发行版都相同。一般来说,它们不便携。std::normal_distribution

评论

0赞 kaisong 9/2/2023
感谢您为我的问题提供最直接的解决方案。现在,如果标准在产生循环体复杂度 O(1) 方面失败,那么我认为这对我来说是失控的。
0赞 kaisong 9/2/2023
感谢您在回答中为我评论的问题添加答案。我现在进一步看到你的代码输出与我的不同,并理解这可能因实现而异。标准中是否有普遍相同的发电机?(因为我认为 mt19937 是)
2赞 tbxfreeware 9/2/2023
C++ 标准要求在每个实现中的行为都相同。不幸的是,它对任何随机数分布都没有这样的保证,包括 .所以,是便携式的,但不是。std::mt19937std::normal_distributionstd::mt19937std::normal_distribution