关于c ++:如何简洁,便携,彻底地播种mt19937 PRNG?

How to succinctly, portably, and thoroughly seed the mt19937 PRNG?

我似乎看到了许多答案,其中有人建议使用生成随机数,通常与以下代码一起使用:

1
2
3
4
std::random_device rd;  
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, 5);
dis(gen);

通常这会取代一些"不神圣的憎恶",比如:

1
2
srand(time(NULL));
rand()%6;

我们可以批评旧方法,认为time(NULL)提供低熵,time(NULL)是可预测的,最终结果是不均匀的。

但所有这些都是新的方式:它只是有一个更光亮的外表。

  • rd()返回单个unsigned int。它至少有16位,可能有32位。这还不足以使mt的19937位州成为种子。

  • 使用std::mt19937 gen(rd());gen()(播种32位并查看第一个输出)并不能提供良好的输出分布。7和13永远不能作为第一个输出。两个种子产生0。12粒种子产生1226181350。(链接)

  • std::random_device可以(有时也可以)作为具有固定种子的简单prng来实现。因此,它可能在每次运行时产生相同的序列。(link)这甚至比time(NULL)更糟。

更糟糕的是,尽管前面的代码片段中存在问题,但复制和粘贴它们还是非常容易的。解决这一问题的一些解决方案需要购买大型图书馆,这可能不适合每个人。

鉴于此,我的问题是,如何在C++中简洁、易懂地、彻底地播种MT1937 PRNG?

鉴于上述问题,一个很好的答案是:

  • 必须完全播种MT19937/MT19937 U 64。
  • 不能仅仅依靠std::random_devicetime(NULL)作为熵源。
  • 不应该依靠暴力或其他诽谤。
  • 应该放在一个小的行数,这样它会看起来很好的副本粘贴到一个答案。

思想

  • 我目前的想法是,std::random_device的输出可以与time(NULL)混合(可能通过xor),地址空间随机化得到的值,以及硬编码常量(可以在分布期间设置),以获得最大的熵。

  • std::random_device::entropy()并不能很好地说明std::random_device可能做什么,也可能不做什么。


我认为std::random_device最大的缺陷是,如果没有可用的csprng,它允许确定性回退。这本身就是不使用std::random_device为prng种子的一个很好的理由,因为产生的字节可能是确定性的。不幸的是,它没有提供一个API来查明发生这种情况的时间,或者请求失败而不是低质量的随机数。

也就是说,没有完全可移植的解决方案:但是,有一种体面的、最小的方法。您可以在csprng(定义为下面的sysrandom)周围使用一个最小的包装器来播种prng。

窗户

你可以依靠一个CSPRNG。例如,您可以使用以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bool acquire_context(HCRYPTPROV *ctx)
{
    if (!CryptAcquireContext(ctx, nullptr, nullptr, PROV_RSA_FULL, 0)) {
        return CryptAcquireContext(ctx, nullptr, nullptr, PROV_RSA_FULL, CRYPT_NEWKEYSET);
    }
    return true;
}


size_t sysrandom(void* dst, size_t dstlen)
{
    HCRYPTPROV ctx;
    if (!acquire_context(&ctx)) {
        throw std::runtime_error("Unable to initialize Win32 crypt library.");
    }

    BYTE* buffer = reinterpret_cast<BYTE*>(dst);
    if(!CryptGenRandom(ctx, dstlen, buffer)) {
        throw std::runtime_error("Unable to generate random bytes.");
    }

    if (!CryptReleaseContext(ctx, 0)) {
        throw std::runtime_error("Unable to release Win32 crypt library.");
    }

    return dstlen;
}

类Unix

在许多类Unix的系统上,您应该尽可能使用/dev/urandom(尽管这并不能保证在符合POSIX的系统上存在)。

1
2
3
4
5
6
7
8
size_t sysrandom(void* dst, size_t dstlen)
{
    char* buffer = reinterpret_cast<char*>(dst);
    std::ifstream stream("/dev/urandom", std::ios_base::binary | std::ios_base::in);
    stream.read(buffer, dstlen);

    return dstlen;
}

号其他

如果没有可用的CSPRNG,您可以选择依赖std::random_device。但是,如果可能的话,我会避免这种情况,因为不同的编译器(尤其是mingw)将其作为prng实现(事实上,每次生成相同的序列以提醒人类它不是正确的随机的)。

播种

既然我们有了开销最小的片段,我们就可以生成所需的随机熵位来为我们的prng播种。该示例使用(明显不足的)32位作为prng的种子,您应该增加这个值(这取决于您的csprng)。

1
2
3
std::uint_least32_t seed;    
sysrandom(&seed, sizeof(seed));
std::mt19937 gen(seed);

与Boost的比较

在快速查看源代码后,我们可以看到boost::random_device(真正的csprng)的并行。Boost在Windows上使用MS_DEF_PROV,这是PROV_RSA_FULL的提供程序类型。唯一缺少的就是验证加密上下文,这可以用CRYPT_VERIFYCONTEXT完成。在*nix上,boost使用/dev/urandom。也就是说,这个解决方案是便携式的、经过良好测试的、易于使用的。

Linux专业化

如果您愿意为了安全而牺牲简洁性,那么在Linux 3.17及更高版本以及最近的Solaris上,getrandom是一个很好的选择。getrandom的行为与/dev/urandom的行为相同,但如果内核在引导之后还没有初始化其csprng,则它会阻塞。下面的代码段检测Linux getrandom是否可用,如果不可用,则返回到/dev/urandom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#if defined(__linux__) || defined(linux) || defined(__linux)
#   // Check the kernel version. `getrandom` is only Linux 3.17 and above.
#   include <linux/version.h>
#   if LINUX_VERSION_CODE >= KERNEL_VERSION(3,17,0)
#       define HAVE_GETRANDOM
#   endif
#endif

// also requires glibc 2.25 for the libc wrapper
#if defined(HAVE_GETRANDOM)
#   include <sys/syscall.h>
#   include <linux/random.h>

size_t sysrandom(void* dst, size_t dstlen)
{
    int bytes = syscall(SYS_getrandom, dst, dstlen, 0);
    if (bytes != dstlen) {
        throw std::runtime_error("Unable to read N bytes from CSPRNG.");
    }

    return dstlen;
}

#elif defined(_WIN32)

// Windows sysrandom here.

#else

// POSIX sysrandom here.

#endif

。打开BSD

最后还有一个警告:现代OpenBSD没有/dev/urandom。您应该改为使用get熵。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#if defined(__OpenBSD__)
#   define HAVE_GETENTROPY
#endif

#if defined(HAVE_GETENTROPY)
#   include <unistd.h>

size_t sysrandom(void* dst, size_t dstlen)
{
    int bytes = getentropy(dst, dstlen);
    if (bytes != dstlen) {
        throw std::runtime_error("Unable to read N bytes from CSPRNG.");
    }

    return dstlen;
}

#endif

其他想法

如果您需要加密安全的随机字节,您可能应该用POSIX的无缓冲打开/读取/关闭替换fstream。这是因为basic_filebufFILE都包含一个内部缓冲区,该缓冲区将通过标准分配器进行分配(因此不会从内存中擦除)。

这可以通过将sysrandom改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
size_t sysrandom(void* dst, size_t dstlen)
{
    int fd = open("/dev/urandom", O_RDONLY);
    if (fd == -1) {
        throw std::runtime_error("Unable to open /dev/urandom.");
    }
    if (read(fd, dst, dstlen) != dstlen) {
        close(fd);
        throw std::runtime_error("Unable to read N bytes from CSPRNG.");
    }

    close(fd);
    return dstlen;
}

。谢谢

特别感谢BenVoigt指出FILE使用缓冲读取,因此不应使用。

我还要感谢PeterCordes提到getrandom,以及OpenBSD缺少/dev/urandom


从某种意义上说,这是不可移植的。也就是说,人们可以构想一个运行C++的有效的完全确定的平台(例如,一个模拟地确定机器时钟的模拟器),以及一个"确定的"I/O),其中没有随机的来源来产生PRNG。


您可以使用std::seed_seq,并使用Alexander Huszagh的熵获取方法将其填充到发电机所需的状态大小:

1
2
3
4
5
6
7
8
9
10
11
size_t sysrandom(void* dst, size_t dstlen); //from Alexander Huszagh answer above

void foo(){

    std::uint_fast32_t[std::mt19937::state_size] state;
    sysrandom(state, sizeof(state));
    std::seed_seq s(std::begin(state), std::end(state));

    std::mt19937 g;
    g.seed(s);
}

如果有合适的方法从标准库中的univerrandombitgenerator填充或创建一个seedsequence,那么使用std::random_device进行正确的播种就简单多了。


我正在进行的实现利用mt19937prng的state_size属性来决定初始化时要提供多少种子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using Generator = std::mt19937;

inline
auto const& random_data()
{
    thread_local static std::array<typename Generator::result_type, Generator::state_size> data;
    thread_local static std::random_device rd;

    std::generate(std::begin(data), std::end(data), std::ref(rd));

    return data;
}

inline
Generator& random_generator()
{
    auto const& data = random_data();

    thread_local static std::seed_seq seeds(std::begin(data), std::end(data));
    thread_local static Generator gen{seeds};

    return gen;
}

template<typename Number>
Number random_number(Number from, Number to)
{
    using Distribution = typename std::conditional
    <
        std::is_integral<Number>::value,
        std::uniform_int_distribution<Number>,
        std::uniform_real_distribution<Number>
    >::type;

    thread_local static Distribution dist;

    return dist(random_generator(), typename Distribution::param_type{from, to});
}

我认为还有改进的余地,因为std::random_device::result_type在尺寸和范围上可能与std::mt19937::result_type有所不同,因此应该真正考虑到这一点。

关于STD:一个随机设备的注释。

根据C++11(/14/17)标准:

26.5.6 Class random_device [ rand.device ]

2 If implementation limitations prevent generating non-deterministic random numbers, the implementation may employ a random number engine.

这意味着,如果由于某些限制而阻止实现生成不确定的值,那么它只能生成确定的值。

Windows上的MinGW编译器不提供来自其std::random_device的非确定性值,尽管它们很容易从操作系统获得。所以我认为这是一个bug,不太可能在实现和平台中出现。


使用时间播种没有什么问题,假设你不需要它来保证安全(而且你没有说这是必要的)。关键是你可以使用散列来修复非随机性。我已经发现这在所有情况下都是有效的,尤其是对于重型蒙特卡罗模拟。

这种方法的一个很好的特性是,它推广到来自其他非真正随机种子集的初始化。例如,如果您希望每个线程都有自己的RNG(用于线程安全性),则可以仅根据哈希线程ID进行初始化。

以下是从我的代码库中提取的SSCC(为了简单起见,省略了一些OO支持结构):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <cstdint> //`uint32_t`
#include <functional> //`std::hash`
#include <random> //`std::mt19937`
#include <iostream> //`std::cout`

static std::mt19937 rng;

static void seed(uint32_t seed) {
    rng.seed(static_cast<std::mt19937::result_type>(seed));
}
static void seed() {
    uint32_t t = static_cast<uint32_t>( time(nullptr) );
    std::hash<uint32_t> hasher; size_t hashed=hasher(t);
    seed( static_cast<uint32_t>(hashed) );
}

int main(int /*argc*/, char* /*argv*/[]) {
    seed();
    std::uniform_int_distribution<> dis(0, 5);
    std::cout << dis(rng);
}


以下是我自己在这个问题上的一点见解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <random>
#include <chrono>
#include <cstdint>
#include
#include <functional>
#include <iostream>

uint32_t LilEntropy(){
  //Gather many potential forms of entropy and XOR them
  const  uint32_t my_seed = 1273498732; //Change during distribution
  static uint32_t i = 0;        
  static std::random_device rd;
  const auto hrclock = std::chrono::high_resolution_clock::now().time_since_epoch().count();
  const auto sclock  = std::chrono::system_clock::now().time_since_epoch().count();
  auto *heap         = malloc(1);
  const auto mash = my_seed + rd() + hrclock + sclock + (i++) +
    reinterpret_cast<intptr_t>(heap)    + reinterpret_cast<intptr_t>(&hrclock) +
    reinterpret_cast<intptr_t>(&i)      + reinterpret_cast<intptr_t>(&malloc)  +
    reinterpret_cast<intptr_t>(&LilEntropy);
  free(heap);
  return mash;
}

//Fully seed the mt19937 engine using as much entropy as we can get our
//hands on
void SeedGenerator(std::mt19937 &mt){
  std::uint_least32_t seed_data[std::mt19937::state_size];
  std::generate_n(seed_data, std::mt19937::state_size, std::ref(LilEntropy));
  std::seed_seq q(std::begin(seed_data), std::end(seed_data));
  mt.seed(q);
}

int main(){
  std::mt19937 mt;
  SeedGenerator(mt);

  for(int i=0;i<100;i++)
    std::cout<<mt()<<std::endl;
}

这里的想法是使用xor结合许多潜在的熵源(快速时间、慢速时间、std::random-device、静态变量位置、堆位置、函数位置、库位置、程序特定值),尽最大努力初始化mt19937。只要源代码至少有一次是"好的",结果至少是"好的"。

这个答案不够简短,可能包含一个或多个逻辑错误。所以我认为这是一项正在进行中的工作。如果您有反馈,请发表评论。


一个给定的平台可能有一个熵源,比如/dev/random。自std::chrono::high_resolution_clock::now()时代以来的纳秒可能是标准库中最好的种子。

我以前使用过类似于(uint64_t)( time(NULL)*CLOCKS_PER_SEC + clock() )的东西来为不安全关键的应用程序获取更多的熵位。