web-dev-qa-db-ja.com

mt19937 PRNGを簡潔、移植可能、および完全にシードする方法は?

私は誰かが<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)は予測可能であり、最終結果は不均一であると主張することで、 criticize を使用できます。

しかし、それはすべて新しい方法に当てはまります。光沢のあるベニアが付いているだけです。

  • rd()は、単一のunsigned intを返します。これには少なくとも16ビット、おそらく32ビットがあります。それはMTの19937ビットの状態をシードするには不十分です。

  • std::mt19937 gen(rd());gen()(32ビットでシードし、最初の出力を見る)を使用しても、適切な出力分布が得られません。 7と13が最初の出力になることはありません。 2つのシードは0を生成します。12のシードは1226181350を生成します。( Link

  • std::random_deviceは、固定シードを使用した単純なPRNGとして実装でき、場合によっては実装されます。したがって、実行ごとに同じシーケンスが生成される場合があります。 ( リンク )これはtime(NULL)よりもさらに悪い。

さらに悪いことに、上記のコードスニペットをコピーアンドペーストすることは、それらに含まれる問題にもかかわらず非常に簡単です。これに対するいくつかの解決策は largishlibraries を取得する必要がありますが、これはすべての人に適しているわけではありません。

これを踏まえて、私の質問はC++でmt19937 PRNGを簡潔かつ移植性の高い方法で完全にシードするにはどうすればよいですか?

上記の問題を考えると、良い答え:

  • Mt19937/mt19937_64を完全にシードする必要があります。
  • エントロピーのソースとしてstd::random_deviceまたはtime(NULL)のみに依存することはできません。
  • Boostまたは他のライブラリに依存しないでください。
  • 回答にコピーアンドペーストされたように見えるように、少数の行に収まる必要があります。

思考

  • 私の現在の考えは、std::random_deviceからの出力は、time(NULL)アドレス空間のランダム化 から派生した値、およびハードコードされた定数(これはXORを介して)エントロピーでベストエフォートショットを取得するために、配信中に設定できます)。

  • std::random_device::entropy()しないstd::random_deviceが実行する場合と実行しない場合があることを適切に示します。

106
Richard

std::random_deviceの最大の欠点は、CSPRNGが利用できない場合に決定論的なフォールバックが許可されることです。生成されるバイトは確定的である可能性があるため、これだけでstd::random_deviceを使用してPRNGをシードしないことをお勧めします。残念ながら、これがいつ発生するかを調べるAPIや、低品質の乱数の代わりに失敗をリクエストするAPIは提供していません。

つまり、完全なポータブルソリューションはありません。ただし、まともな最小限のアプローチがあります。 PRNGをシードするには、CSPRNG(以下のsysrandomとして定義)の周りに最小限のラッパーを使用できます。


CSPRNGであるCryptGenRandomを使用できます。たとえば、次のコードを使用できます。

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との比較


ソースコード をざっと見ると、boost :: random_device(真のCSPRNG)の類似点がわかります。 BoostはMS_DEF_PROVのプロバイダータイプであるWindowsでPROV_RSA_FULLを使用します。欠けているのは、暗号コンテキストの検証だけです。これは、CRYPT_VERIFYCONTEXTで実行できます。 * Nixでは、Boostは/dev/urandomを使用します。 IE、このソリューションは移植性があり、十分にテストされており、使いやすいです。

Linuxスペシャライゼーション


セキュリティのために簡潔さを犠牲にする場合は、Linux 3.17以降および最近のSolarisで getrandom が最適な選択です。 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


最後の警告が1つあります。最新の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;
}

ありがとう


FILEがバッファリングされた読み取りを使用していることを指摘してくれたBen Voigtに感謝します。したがって、使用しないでください。

getrandomとOpenBSDの/dev/urandomの欠如に言及してくれたPeter Cordesにも感謝したいと思います。

55

ある意味では、これを移植性のある方法で行うことはできません。つまり、C++を実行する有効な完全決定論的プラットフォーム(たとえば、マシンクロックを決定論的に、「決定化された」I/Oでステップするシミュレーター)を想像できます。

22
einpoklum

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

適切にシードするためにstd::random_deviceを使用する標準ライブラリで niformRandomBitGenerator から SeedSequence を入力または作成する適切な方法があれば、はるかに簡単になります。

12
ratchet freak

私が取り組んでいる実装は、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実装の制限により非決定論的な乱数の生成が妨げられる場合、実装は乱数エンジンを使用できます。

これは、何らかの制限によりnon-deterministicの生成が妨げられている場合にのみ、実装がdeterministicの値を生成できることを意味します。

MinGW上のWindowsコンパイラは、オペレーティングシステムから簡単に入手できるにもかかわらず、std::random_deviceからnon-deterministic値を提供しないことで有名です。したがって、これはバグであり、実装およびプラットフォーム間での一般的な発生ではないと考えています。

4
Galik

時間を使用することで問題が発生することはありません。安全である必要はないと仮定します(そして、これが必要だとは言わなかった)。洞察は、ハッシュを使用して非ランダム性を修正できるということです。重いモンテカルロシミュレーションを含むすべての場合に、これが適切に機能することがわかりました。

このアプローチの優れた機能の1つは、実際にはランダムではない他のシードセットからの初期化に一般化されることです。たとえば、各スレッドに独自の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);
}
2
imallett

質問に私自身の刺しがあります:

#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の初期化でベストエフォートの試みを行う。少なくとも1回ソースが「良好」である限り、結果は少なくとも「良好」になります。

この答えは望ましいほど短いものではなく、1つ以上のロジックの間違いが含まれている場合があります。それで、私はそれを進行中の仕事と考えています。フィードバックがある場合はコメントしてください。

0
Richard