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

时间:2017-07-12 23:44:19

标签: c++ c++11 random

我似乎看到很多答案,有人建议使用<random>生成随机数,通常还会附带以下代码:

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

通常这会取代某种不圣洁的憎恶&#34;如:

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

我们可能criticize认为time(NULL)提供低熵,time(NULL)是可预测的,最终结果是不均匀的。{/ p>

但所有这一切都适用于新的方式:它只是有一个更光亮的贴面。

  • 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)更糟糕。

更糟糕的是,尽管存在问题,但复制和粘贴上述代码片段非常容易。对此的一些解决方案需要获取可能不适合所有人的largish libraries

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

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

  • 必须完全播种mt19937 / mt19937_64。
  • 不能仅依靠std::random_devicetime(NULL)作为熵来源。
  • 不应该依赖Boost或其他图书馆。
  • 应该适合少量的线条,以便将其复制粘贴到答案中。

思想

  • 我目前的想法是std::random_device的输出可以与time(NULL)混合(可能通过XOR),从address space randomization派生的值和硬编码常量(这可以在分发期间设置)以获得熵的最佳努力。

  • std::random_device::entropy() does not可以很好地说明std::random_device可能做什么或不做什么。

8 个答案:

答案 0 :(得分:54)

我认为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后,我们可以看到与:: 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时阻塞。以下代码段会检测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

其他想法

如果您需要加密安全的随机字节,您应该用POSIX的无缓冲打开/读取/关闭替换fstream。这是因为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

答案 1 :(得分:22)

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

答案 2 :(得分:12)

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

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

如果有正确的方法从标准库中的SeedSequence填充或创建UniformRandomBitGenerator,使用std::random_device进行正确播种会更加简单。

答案 3 :(得分:4)

我正在开发的实现利用state_size PRNG的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编译器并不提供@network_hosts中的非确定性值,尽管它们可以从操作系统轻松获得。所以我认为这是一个错误,并且不太可能在实现和平台上发生。

答案 4 :(得分:2)

使用时间播种并没有错,假设您不需要它是安全的(并且您没有说这是必要的)。洞察力是您可以使用散列来修复非随机性。我发现这在所有情况下都适用,包括特别是重型蒙特卡罗模拟。

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

答案 5 :(得分: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。只要至少有一次来源是好的&#34;,结果将至少是&#34;好&#34;。

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

答案 6 :(得分:0)

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

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

答案 7 :(得分:-2)

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

我之前使用(uint64_t)( time(NULL)*CLOCKS_PER_SEC + clock() )这样的东西为非安全关键的应用程序获取更多的熵。