如何简洁、便携、彻底地为 mt19937 PRNG 播种

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

本文关键字:mt19937 播种 PRNG 便携 何简洁 简洁      更新时间:2023-10-16

我似乎看到很多答案,其中有人建议使用<random>生成随机数,通常伴随着这样的代码:

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

通常这取代了某种"不圣洁的可憎之物",例如:

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.十二颗种子产生1226181350。(链接)

  • std::random_device可以,有时是作为具有固定种子的简单PRNG实现的。因此,它可能在每次运行时生成相同的序列。(链接)这甚至比time(NULL)还要糟糕.

更糟糕的是,复制和粘贴上述代码片段非常容易,尽管它们包含问题。一些解决方案需要获取可能不适合所有人的庞大库。

有鉴于此,我的问题是,如何在C++中简洁、便携、彻底地播种mt19937 PRNG?

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

  • 必须完全为 mt19937/mt19937_64 设定种子。
  • 不能仅仅依靠std::random_devicetime(NULL)作为熵的来源。
  • 不应依赖 Boost 或其他库。
  • 应该适合少量行,以便复制粘贴到答案中看起来不错。

思潮

  • 我目前的想法是,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 设定种子。

窗户


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

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的系统上)。

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)。

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

提升比较


在快速浏览源代码后,我们可以看到 boost::random_device(真正的 CSPRNG)的相似之处。Boost 在 Windows 上使用MS_DEF_PROV,这是PROV_RSA_FULL的提供程序类型。唯一缺少的是验证加密上下文,这可以通过CRYPT_VERIFYCONTEXT.在 *Nix 上,Boost 使用/dev/urandom。IE,此解决方案是可移植的,经过良好测试且易于使用。

Linux 专业化


如果您愿意为了安全性而牺牲简洁性,那么在 Linux 3.17 及更高版本以及最近的 Solaris 上,getrandom是一个很好的选择。getrandom的行为与/dev/urandom相同,除了如果内核在启动后尚未初始化其CSPRNG,则会阻塞。 以下代码片段检测 Linuxgetrandom是否可用,如果没有,则回退到/dev/urandom

#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

OpenBSD


还有最后一个警告:现代OpenBSD没有/dev/urandom。你应该改用getentropy。

#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

其他想法


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

这可以通过将sysrandom更改为:

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

谢谢


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

我还要感谢 Peter Cordes 提到getrandom,以及 OpenBSD 缺乏/dev/urandom

从某种意义上说,这不能通过移植来完成。也就是说,人们可以设想一个有效的完全确定性平台运行C++(例如,一个确定性地步进机器时钟的模拟器,并具有"确定的"I/O),其中没有随机性来源来播种PRNG。

您可以使用一个std::seed_seq并使用Alexander Huszagh获取熵的方法将其填充到生成器所需的状态大小:

size_t sysrandom(void* dst, size_t dstlen); //from Alexander Huszagh answer above
void foo(){
std::array<std::mt19937::UIntType, std::mt19937::state_size> state;
sysrandom(state.begin(), state.length*sizeof(std::mt19937::UIntType));
std::seed_seq s(state.begin(), state.end());
std::mt19937 g;
g.seed(s);
}

如果有一种方法可以使用std::random_device从标准库中的 UniformRandomBitGenerator 填充或创建 SeedSequence 正确种子,那就简单多了。

我正在处理的实现利用mt19937PRNG 的state_size属性来决定在初始化时提供多少种子:

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::random_device 的说明。

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

26.5.6random_device [兰德设备]

2如果实现限制阻止生成非确定性随机数,则实现可以采用随机数引擎。

这意味着如果实现由于某些限制而无法生成非确定性值,则只能生成确定性值。

MinGW编译器Windows

著名的没有从其std::random_device提供非确定性值,尽管它们很容易从操作系统中获得。因此,我认为这是一个错误,不太可能在实现和平台上常见。

使用时间进行播种并没有错,假设你不需要它是安全的(你没有说这是必要的)。 见解是您可以使用哈希来修复非随机性。 我发现这在所有情况下都足够有效,特别是对于繁重的蒙特卡罗模拟。

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

以下是从我的代码库中提炼出来的SSCCE(为简单起见;省略了一些OO支持结构):

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

这里已经有很多很好的答案,但我想补充两件事:

  • MinGW错误,这里被引用为确定性std::random_device实现的最显着的例子,在最新版本中得到了修复(链接到错误报告)。

  • 在 c++20 中,有一种方法可以在不使用缓冲数组的情况下用std::random_device中的值填充std::seed_seq

#include <random>
#include <ranges>
int main(){
std::random_device rd;
auto rd_range = std::ranges::transform_view(std::ranges::iota_view(static_cast<std::size_t>(0), std::mt19937::state_size), [&rd](size_t){return rd();});
std::seed_seq seeds(rd_range.begin(), rd_range.end());
std::mt19937 gen(seeds);
return 0;
}

这是我自己对这个问题的看法:

#include <random>
#include <chrono>
#include <cstdint>
#include <algorithm>
#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。只要至少有一个来源是"好的",结果至少会是那个"好"。

这个答案并不像最好的那样短,可能包含一个或多个逻辑错误。所以我正在考虑这是一项正在进行的工作。如果您有反馈,请发表评论。

  • 使用 getentropy() 为伪随机数生成器 (PRNG) 播种。
  • 如果你想要随机值(而不是,比如说,/dev/urandom/dev/random),请使用getrandom()。

这些可以在现代类UNIX系统上使用,例如Linux,Solaris和OpenBSD。

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

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