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

Posted

技术标签:

【中文标题】如何简洁、便携和彻底地播种 mt19937 PRNG?【英文标题】:How to succinctly, portably, and thoroughly seed the mt19937 PRNG? 【发布时间】:2017-12-17 13:30:25 【问题描述】:

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

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

通常这会取代某种“邪恶的可憎之物”,例如:

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

我们可能会 criticize 以旧方式争论 time(NULL) 提供低熵,time(NULL) 是可预测的,并且最终结果是不均匀的。

但新方式的所有这些都是真实的:它只是有一个更闪亮的饰面。

rd() 返回单个 unsigned int。这至少有 16 位,可能有 32 位。这不足以播种 MT 的 19937 位状态。

使用std::mt19937 gen(rd());gen()(使用 32 位播种并查看第一个输出)不会提供良好的输出分布。 7 和 13 永远不能是第一个输出。两颗种子产生 0。十二颗种子产生 1226181350。(Link)

std::random_device 可以,有时也可以实现为带有固定种子的简单 PRNG。因此,它可能会在每次运行时产生相同的序列。 (Link) 这比time(NULL)还要糟糕。

更糟糕的是,复制和粘贴上述代码 sn-ps 非常容易,尽管它们包含问题。对此的一些解决方案需要获取largish libraries,这可能并不适合所有人。

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

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

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

想法

我目前的想法是,std::random_device 的输出可以与time(NULL)、address space randomization 派生的值和硬编码常量(可以在分发期间设置) 以尽最大努力获得熵。

std::random_device::entropy() does not 很好地表明了 std::random_device 可能会或可能不会做什么。

【问题讨论】:

我个人的想法是,也许可以从std::random_devicetime(NULL)和函数地址中提取值,然后异或一起产生一种尽力而为的熵源。 如果有类似 dos_random_device_actually_work() 这样的函数就好了,这样至少可以优雅地降级,或者为用户产生警告或错误。 正解不短,短解不正。我在seed11 library 中使用的方法基本上是在您计划运行程序的平台上正确实现std::random_device,并提供一个辅助函数来创建种子生成器(seed11::make_seeded&lt;std::mt19937&gt;() @NeilButterworth:这并不能识别是否正在使用静态种子 PRNG,就像 MinGW 中的情况一样。 旁白:您的第二个项目符号没有添加任何新内容。您发现某个值出现 12 次也就不足为奇了。 You should expect there to be just over three values that appear exactly 12 times,假设您有 2^32 个独立、均匀随机样本。 【参考方案1】:

我认为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);

提升对比


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

Linux 专业化


如果您愿意为了安全而牺牲简洁性,getrandom 是 Linux 3.17 及更高版本以及最近的 Solaris 上的绝佳选择。 getrandom 的行为与 /dev/urandom 相同,但如果内核在启动后尚未初始化其 CSPRNG,它会阻塞。下面的 sn -p 检测 Linux getrandom 是否可用,如果不可用则回退到 /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

【讨论】:

这是我过去所做的,但问题是,或者至少是一个问题,WTF 不能为这些平台的库编写者为我们做这件事吗?我希望文件访问和线程(例如)被库实现抽象出来,那么为什么不生成随机数呢? OP here:如果这个答案能更好地证明播种会更好。尽可能地,我希望生成可复制粘贴代码的答案比我在问题中发布的简单示例做得更好,而不需要编码人员进行太多技术解释或思考。 我认为/dev/random 将是播种 RNG 的更好选择,但显然/dev/urandom is still considered computationally secure 即使/dev/random 会因为可用熵低而阻塞,所以urandom 是推荐的选择除了一次性垫之外的所有东西。另见unix.stackexchange.com/questions/324209/…。不过,请注意在启动后很早就来自urandom 的可预测种子。 Linux 的getrandom(2) 系统调用就像打开和读取/dev/urandom 一样,只是如果内核的随机源尚未初始化,它会阻塞。我认为这可以让您摆脱早期启动的低质量随机性问题,而不会在 /dev/random 等其他情况下阻塞。 @PeterCordes,当然,这是一个很好的选择。但是,它不适用于 BSD 或其他 *Nixes,这是 /dev/urandom 通常适用的。我通常订阅关于此的 Python 邮件列表讨论:bugs.python.org/issue27266【参考方案2】:

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

【讨论】:

@kbelder: 1. 谁说用户是人? 2. 不是所有的程序都有用户交互,你当然不能假设周围总是有一个用户...... 我很欣赏这个回应,但也觉得程序应该做出合理的尽力尝试。 @Richard 同意,但问题是 C++ 标准编写者必须(或至少尽力)适应这些奇怪的情况。这就是为什么你会得到这些空洞的标准定义,在那里你可能会得到不错的结果,但编译器仍然可以符合标准,即使它返回的东西在功能上毫无价值。 - 因此,您的限制(“短且不能依赖其他库”)排除了任何响应,因为您实际上需要逐个平台/逐个编译器的特殊外壳。 (例如,Boost 做得这么好。) @Richard 然而,它解释的是,你得到了你在标准中得到的东西因为没有可移植的方法可以做得更好。如果您想做得更好(这是一个崇高的目标),您将不得不接受或多或少的憎恶:) @Richard:有时你只需要接受一个符合标准的 C++ 实现是可能的,但它并没有什么用处。由于人们用于任何重要事物的实现 被设计为有用,因此您有时不得不忍受诸如“任何理智的实现都会做一些合理的事情”之类的论点。我本来希望std::random_device 属于该类别,但如果某些实际实现使用固定种子 PRNG,显然不是这样!这远远超出了 einpoklum 的论点。【参考方案3】:

您可以使用 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 填充或创建SeedSequence 以正确播种会简单得多。

【讨论】:

seed_seq 有问题,pcg-random.org/posts/developing-a-seed_seq-alternative.html C++ 标准中没有任何内容可以保证当您从 seed_seq 播种时随机数生成器将使用整个数组。如果您将 rng 用于科学模拟,显然也用于密码学,此方法将导致失败。唯一的用例是随机化一个视频游戏,但这会有点过头了。【参考方案4】:

我正在处理的实现利用mt19937 PRNG 的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 genseeds;

    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_typefrom, to);

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

关于std::random_device的说明。

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

26.5.6 类 random_device [ rand.device ]

2如果实施限制阻止生成不确定的随机数,则实施可能会使用随机数引擎。

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

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

【讨论】:

这可能会填充 MT 状态,但仍仅依赖于 std::random_device,因此容易受到由此产生的问题的影响。 我想我在问题中已经足够清楚地说明了它们。不过,很高兴澄清/讨论。 @Richard 是否有任何实际系统没有真正实现合理的std::random_device?我知道标准允许PRNG 作为后备,但我觉得这只是为了掩盖自己,因为很难要求使用C++ 的每个设备都有一个不确定的随机源。如果他们不这样做,那么你能做些什么呢? @AlexanderHuszagh 我不太确定。我的意图是让我的“便携式解决方案”依赖于设备,因为如果设备支持非确定性生成器,那么std::random_device 也应该支持。我相信这就是标准的精神。所以我搜索了,只能找到在这方面被破坏的MinGW。似乎没有人用我发现的任何其他东西报告这个问题。因此,在我的库中,我只是将MinGW 标记为不受支持。如果有更广泛的问题,那么我会重新考虑。我只是现在没有看到这方面的证据。 我真的很失望 MinGW 以不提供平台随机功能的形式提供给每个人的 std::random_device。低质量的实现违背了现有 API 的目的。如果他们在它工作之前根本不实施它,那将是更好的 IMO。 (或者更好的是,如果 API 提供了一种在高质量随机性不可用时请求失败的方法,那么 MinGW 可以避免造成安全风险,同时仍然为游戏或其他提供不同的种子。)【参考方案5】:

假设您不需要时间来保证安全(并且您没有说这是必要的),那么使用时间播种并没有错。洞察力是您可以使用散列来修复非随机性。我发现这在所有情况下都非常有效,尤其是在繁重的蒙特卡洛模拟中。

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

以下是SSCCE,从my codebase 提炼而来(为简单起见;省略了一些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);

【讨论】:

我同意你的观点,如果你不需要它来保证安全,那么在实践中用时间播种可能就足够了。但我不能同意你其余的回答。使用时间哈希播种并不比使用时间本身播种更好。 @D.W.根据经验,它要好得多。原因是哈希是不连续的,并且跨越了更广泛的值(自己试试这个:用12 做种子,观察它们生成的浮点序列需要一段时间才能真正发散)。跨度> 我不明白为什么这很重要。我们一次只运行一个种子。种子的可能值空间(种子的熵)是相同的——散列不会增加熵。也许您可以编辑问题以解释为什么散列更好?【参考方案6】:

这是我自己的问题:

#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。只要至少有一次来源是“好”的,结果就会至少是那个“好”。

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

【讨论】:

地址的随机性可能很小。你总是有相同的分配,所以在你可以访问整个内存的较小的嵌入式系统上,每次都可能得到相同的结果。我想说它对于大型系统来说可能已经足够好了,但在微控制器上可能会很糟糕。 我猜&amp;i ^ &amp;myseed 的熵应该比单独的一个要小得多,因为两者都是在同一个翻译单元中具有静态存储持续时间的对象,因此可能非常接近。而且您似乎并没有真正使用myseed初始化中的特殊值? 将释放的指针转换为整数是未定义的行为;在它仍然存在的时候去做。 ^ 是一个可怕的哈希组合器;如果两个值都具有很多熵,但彼此相比很少,则将其删除。 + 通常更好(因为 x+x 只在 x 中燃烧 1 位熵,而 x^x 将它们全部燃烧)。我怀疑函数不安全 (rd()) 哦,+ 我的意思是未签名(+ 签名是 UB-bait)。虽然这些是有些荒谬的 UB 案例,但您确实说过便携。如果可能,还要考虑将函数的地址作为整数值(不确定是否是?) @meneldal:即使在全功能 PC 上,虽然分配可能会获得不同的物理位置(取决于进程外部机器的状态),但指针由进程虚拟地址空间抽象,并且可能具有高度可重复性,尤其是 ASLR 没有生效。【参考方案7】: 使用 getentropy() 播种伪随机数生成器 (PRNG)。 如果您想要随机值(而不是 /dev/urandom/dev/random),请使用 getrandom()。

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

【讨论】:

【参考方案8】:

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

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

【讨论】:

你真的应该使用/dev/urandom,尤其是在这种情况下。 /dev/random 块,并且通常没有充分的理由这样做([插入关于有多少不同操作系统估计 /dev/random 产生的字节的随机性的详细解释])。 @AlexanderHuszagh 没错,虽然我不得不在不存在 /dev/urandom 的系统上编写代码,而阻塞的替代方案是确定性。一个盒子可能也有/dev/hwrng/dev/hw_random,这应该会更好。 好吧,我说,“比如/dev/random”,这似乎引发了一场关于Linux 上/dev/random/dev/urandom 的圣战,我在举这个例子时并不打算这样做..

以上是关于如何简洁、便携和彻底地播种 mt19937 PRNG?的主要内容,如果未能解决你的问题,请参考以下文章

C++11使用mt19937和uniform_int_distributio 生成随机数

mt19937

伪随机数生成算法-梅森旋转(Mersenne Twister/MT)

随机化吼啊!

Mersenne Twister 与 Mersenne Twister 64 位

GCC非法指令