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; |
号
我们可以批评旧方法,认为
但所有这些都是新的方式:它只是有一个更光亮的外表。
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_device 或time(NULL) 作为熵源。 - 不应该依靠暴力或其他诽谤。
- 应该放在一个小的行数,这样它会看起来很好的副本粘贴到一个答案。
思想
我目前的想法是,
std::random_device 的输出可以与time(NULL) 混合(可能通过xor),地址空间随机化得到的值,以及硬编码常量(可以在分布期间设置),以获得最大的熵。std::random_device::entropy() 并不能很好地说明std::random_device 可能做什么,也可能不做什么。
我认为
也就是说,没有完全可移植的解决方案:但是,有一种体面的、最小的方法。您可以在csprng(定义为下面的
你可以依靠一个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,您可以选择依赖
既然我们有了开销最小的片段,我们就可以生成所需的随机熵位来为我们的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上使用
如果您愿意为了安全而牺牲简洁性,那么在Linux 3.17及更高版本以及最近的Solaris上,
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没有
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。这是因为
这可以通过将
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指出
我还要感谢PeterCordes提到
从某种意义上说,这是不可移植的。也就是说,人们可以构想一个运行C++的有效的完全确定的平台(例如,一个模拟地确定机器时钟的模拟器),以及一个"确定的"I/O),其中没有随机的来源来产生PRNG。
您可以使用
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,那么使用
我正在进行的实现利用
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:一个随机设备的注释。
根据
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.
号
这意味着,如果由于某些限制而阻止实现生成不确定的值,那么它只能生成确定的值。
使用时间播种没有什么问题,假设你不需要它来保证安全(而且你没有说这是必要的)。关键是你可以使用散列来修复非随机性。我已经发现这在所有情况下都是有效的,尤其是对于重型蒙特卡罗模拟。
这种方法的一个很好的特性是,它推广到来自其他非真正随机种子集的初始化。例如,如果您希望每个线程都有自己的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结合许多潜在的熵源(快速时间、慢速时间、
这个答案不够简短,可能包含一个或多个逻辑错误。所以我认为这是一项正在进行中的工作。如果您有反馈,请发表评论。
一个给定的平台可能有一个熵源,比如
我以前使用过类似于