如何简洁,可移植和彻底播种mt19937 PRNG?


112

我似乎看到了许多答案,有人建议使用这些答案<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可能做什么或可能不会做什么。


24
@Fabien:那是什么可移植的?这是一个C ++问题,而不是Linux问题。
Lightness Races in Orbit

6
我个人的想法是,也许可以从std::random_devicetime(NULL)和函数地址中提取值,然后将它们异或以产生一种尽力而为的熵源。
理查德

5
如果有诸如dos_random_device_actually_work()之类的功能,那将是很好的选择,这样至少可以优雅地降级,或者为用户产生警告或错误。

4
正确的解决方案不是简短的,简短的解决方案是不合适的。我在seed11库中使用的方法基本上std::random_device是在计划运行程序的平台上正确实现,并提供一个辅助函数来创建种子生成器(seed11::make_seeded<std::mt19937>()
milleniumbug

5
撇开:您的第二个子弹不会添加任何新内容。毫不奇怪,您发现一些价值出现了12倍。假设您有2 ^ 32个独立的,均匀随机的样本,则应该期望有3个值正好出现12次

Answers:


58

我认为最大的缺点std::random_device是,如果没有CSPRNG,则允许确定性回退。仅此一个很好的理由,不要使用来播种PRNG std::random_device,因为产生的字节可能是确定的。不幸的是,它没有提供API来找出发生这种情况的时间,也没有提供请求失败的API,而不是低质量的随机数。

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

视窗


您可以依靠CryptGenRandomCSPRNG。例如,您可以使用以下代码:

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 MS_DEF_PROV在Windows上使用,这是的提供程序类型PROV_RSA_FULL。唯一缺少的是验证密码上下文,可以使用完成CRYPT_VERIFYCONTEXT。在* Nix上,Boost使用/dev/urandom。IE,该解决方案是便携式的,经过测试的且易于使用的。

Linux专业化


如果您愿意为了安全而牺牲简洁性,getrandom那么在Linux 3.17和更高版本以及最新的Solaris上,这是一个绝佳的选择。getrandom的行为与相同/dev/urandom,不同之处在于,如果内核在引导后尚未初始化CSPRNG,则会阻塞。以下代码段检测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


11
这是我过去所做的,但是至少一个问题是WTF不能为这些平台的库编写者为我们这样做吗?我希望文件访问和线程(例如)被库实现抽象化,那么为什么不生成随机数呢?

2
OP此处:如果此答案证明播种好一点,那就太好了。我希望尽可能多的答案能生成可复制复制的代码,而不是我在问题中发布的简单示例,而不需要编码人员进行过多的技术解释或思考,从而可以更好地完成工作。
理查德

4
我认为/dev/random这是播种RNG的更好选择,但显然/dev/urandom仍被认为是计算安全的,即使/dev/random由于较低的可用熵而可能会阻塞,因此urandom建议为除一次性填充以外的所有内容选择。另请参见unix.stackexchange.com/questions/324209/…urandom但是,在启动后的很早就提防可预见的种子。
彼得·科德斯

2
Linux的getrandom(2)系统调用类似于open和read /dev/urandom,但是如果内核的随机性源尚未初始化,它将阻塞。我认为这可以使您免于早期启动的低质量随机性问题,而不会像其他情况那样/dev/random受阻。
彼得·科德斯

1
@PeterCordes,可以肯定的是,这是一个不错的选择。但是,它不能在BSD或其他* Nixes上/dev/urandom正常运行。我通常会订阅有关此内容的Python邮件列表讨论:bugs.python.org/issue27266
Alexander Huszagh

22

从某种意义上说,这是无法移植的。也就是说,可以构想一个运行C ++的有效的完全确定性平台(例如,一个确定性地步进机器时钟并使用“确定性” I / O的模拟器),在该平台中,没有随机来源来植入PRNG。


1
@kbelder:1.谁说用户是一个人?2.并非所有程序都具有用户交互性,您当然不能假设周围总是有用户...
einpoklum

8
我对此表示赞赏,但也觉得程序应该做出合理的努力。
理查德

3
@Richard Agreed,但问题是C ++标准编写者必须(或至少尝试他们的最胆识)来适应这种奇怪的情况。这就是为什么您会得到这些杂乱无章的标准定义的原因,尽管您可能会得到不错的结果,但是即使编译器返回了功能上毫无价值的东西,它仍然可以符合标准。-因此,您的限制(“简短且不能依赖其他库”)排除了任何响应,因为您实际上需要一个逐平台/逐编译器的特殊框。(例如,Boost做得很好。)
RM

2
@Richard的解释是,您可以得到标准中的结果,因为没有便携式的方法可以做得更好。如果您想做得更好(这是一个崇高的目标),您将不得不接受或多或少的可憎之举:)
hobbs

1
@Richard:有时您只需要接受有可能进行无用的符合标准的C ++实现。由于人们在重要的事情上使用的实现设计为有用的,因此有时您必须忍受诸如“任何明智的实现都会做一些合理的事情”之类的争论。我本来希望std::random_device可以属于该类别,但显然不是某些真正的实现使用固定种子PRNG!这远远超出了恩波科姆的论点。
彼得·科德斯

14

您可以使用a,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);
}

如果有填充或创建一个适当的方式SeedSequenceUniformRandomBitGenerator在使用标准库std::random_device播种正确就会简单得多。


1
尽管seed_seq有问题,pcg-random.org
posts /

C ++标准中没有任何内容可以保证当您从seed_seq播种时,随机数生成器将使用整个数组。如果您将rng用于科学模拟,并且显然也用于密码,则此方法将导致失败。关于此的唯一用例是将视频游戏随机化,但这会产生过大的杀伤力。
科斯塔斯

5

我正在研究的实现利用PRNG 的state_size属性mt19937来决定要在初始化时提供多少种子:

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.6类random_device [ rand.device ]

2如果实施局限性阻止生成不确定的随机数,则实施可采用随机数引擎。

这个装置的实施可以仅生成确定性如果从防止生成值非确定性通过一定的局限性的。

众所周知,MinGW编译器Windows不会从其提供不确定的std::random_device,尽管可以从操作系统轻松获得它们。因此,我认为这是一个错误,不太可能在实现和平台之间普遍出现。


1
这可能会填满MT状态,但仍然仅依赖std::random_device,因此很容易受到源于此的问题的影响。
理查德

1
我想我在问题中足够清楚地陈述了它们。不过,很高兴澄清/讨论。
理查德

2
@Richard是否有任何实际系统未真正实现合理的系统std::random_device?我知道标准允许PRNG回退,但是我觉得这只是为了掩盖自己,因为很难要求使用的每个设备都C++具有不确定的随机源。如果他们不这样做,那么您该怎么办?
Galik '17

5
@AlexanderHuszagh我不太确定。我的意图是使我的“便携式解决方案”依赖于该设备,因为如果该设备支持非确定性生成器,则应该如此std::random_device。我相信这是标准的精神。因此,我进行了搜索,只能发现MinGW在这方面已被破坏。似乎没有人报告我发现的任何其他问题。因此,在我的图书馆中,我只是标记MinGW为不支持。如果存在更大的问题,那么我会重新考虑。我只是现在看不到任何证据。
Galik '17

5
我真的很失望,因为MinGW std::random_device以无法提供平台随机性功能的形式提供给每个人,这毁了所有人。低质量的实现无法达到现有API的目的。如果他们在使之工作之前根本不实施它,那将是一个更好的IMO。(或者更好的是,如果API提供了一种在无法获得高质量随机性的情况下请求失败的方法,那么MinGW可以避免造成安全风险,同时仍然为游戏或其他事情提供不同的种子。)
Peter Cordes

2

假设您不需要时间来确保安全(并且您没有说这是必要的),那么通过使用时间进行播种就没有什么不妥。洞察力在于您可以使用哈希来修复非随机性。我发现该方法在所有情况下都可以正常工作,尤其是在大型蒙特卡洛模拟中尤其如此。

这种方法的一个不错的功能是,它可以从其他非真正随机的种子集中推广到初始化。例如,如果您希望每个线程都有自己的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);
}

1
我同意您的观点,即如果您不需要时间来保证时间,那么在实践中播种时间可能就足够了。但是我不同意您的其余回答。用时间散列进行播种并不比用时间本身进行播种更好。
DW

@DW根据经验,它好得多。原因是散列是不连续的,并且跨越了更大的值范围(您自己尝试:使用1和进行种子播种,2并观察它们生成的浮点序列需要一段时间才能真正发散)。
imallett

我不明白为什么这很重要。我们一次只运行一个种子。种子的可能值空间(种子的熵)是相同的-散列不会增加熵。也许您可以编辑问题以解释为什么哈希更好?
DW

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

这个答案并不那么短,可能包含一个或多个逻辑错误。因此,我正在考虑将其进行中。如果您有反馈意见,请发表评论。


3
地址可能具有很小的随机性。您始终具有相同的分配,因此在较小的嵌入式系统上,您可以访问整个内存,每次都可能获得相同的结果。我想说它对于一个大型系统可能已经足够好了,但在微控制器上可能确实很糟糕。
meneldal

1
我猜想&i ^ &myseed熵应该比任何一个都要少得多,因为这两个对象都是在同一翻译单元中具有静态存储持续时间的对象,因此很可能彼此靠近。而且您似乎并没有实际使用初始化中的特殊值myseed
aschepler '17

7
将取消分配的指针转换为int是未定义的行为;在它仍然存在的时候做。 ^是一个可怕的哈希组合器;如果两个值都具有很大的熵,但相比却很少,则将其删除。 +通常会更好(因为x + x仅消耗x中的1熵,而x ^ x消耗所有熵)。我怀疑功能不是安全的(rd()
Yakk-Adam Nevraumont

2
哦,+我的意思是未签名(已+签名是UB-诱饵)。虽然这些都是一些荒谬的UB案例,但您确实说过可移植。如果可能,还可以考虑将函数的地址作为整数值获取(不确定是否是?)
Yakk-Adam Nevraumont

1
@meneldal:即使在全功能的PC上,尽管分配可能会获得不同的物理位置(取决于进程外部计算机的状态),但指针是由进程虚拟地址空间抽象的,并且高度可重复,尤其是ASLR无效。
Ben Voigt

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

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


-2

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

以前,我曾使用过类似的方法(uint64_t)( time(NULL)*CLOCKS_PER_SEC + clock() )来获取对安全性要求不高的应用程序更多的熵。


2
您确实应该使用/dev/urandom,尤其是在这种情况下。/dev/random块,并且这样做通常没有充分的理由([插入关于多少个不同操作系统估计/ dev / random产生的字节的随机性的详细说明])。
Alexander Huszagh '17

2
@AlexanderHuszagh是的,尽管我不得不在/dev/urandom不存在的系统上进行编码,而阻塞的替代方法是确定性。一箱可能有/dev/hwrng/dev/hw_random为好,这应该会更好。
戴维斯洛

好吧,我说“例如/dev/random”,这似乎引发了一场/dev/random/dev/urandomLinux 对抗的神圣战争,当我举这个例子时,我并没有打算
。.– Davislor
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.