Как лаконично, переносимо и основательно семя mn19937 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) является предсказуемым, а конечный результат не является -равномерная.

Но все это верно по-новому: у него просто блестящий шпон.

  • rd() возвращает один unsigned int. У этого есть как минимум 16 бит и, возможно, 32. Этого недостаточно, чтобы вынести бит MT 19937 бит.

  • Использование std::mt19937 gen(rd());gen() (посев с 32 битами и просмотр первого выхода) не дает хорошего распределения выходных данных. 7 и 13 никогда не могут быть первым выходом. Два семена производят 0. Двенадцать семян производят 1226181350. (Ссылка)

  • std::random_device может быть, а иногда и реализован как простой PRNG с фиксированным семенем. Таким образом, при каждом прогоне она может иметь одну и ту же последовательность. (Ссылка) Это еще хуже, чем time(NULL).

Хуже того, очень легко скопировать и вставить вышеприведенные фрагменты кода, несмотря на проблемы, которые они содержат. Некоторые решения для этого требуют приобретения довольно больших библиотек, которые могут не быть подходящим для всех.

В свете этого мой вопрос: Как можно лаконично, переносимо и основательно семенить mt19937 PRNG на С++?

Учитывая вышеизложенные вопросы, хороший ответ:

  • Должен полностью засеять mt19937/mt19937_64.
  • Нельзя полагаться только на std::random_device или time(NULL) в качестве источника энтропии.
  • Не следует полагаться на Boost или другие библиотеки.
  • Должно вписываться в небольшое количество строк, чтобы оно выглядело красиво, скопировано в ответ.

Мысли

Ответ 1

Я бы сказал, что самый большой недостаток с std::random_device - это то, что ему разрешен детерминированный отказ, если нет CSPRNG. Это само по себе является хорошей причиной, чтобы не высевать PRNG с использованием std::random_device, так как полученные байты могут быть детерминированными. К сожалению, он не предоставляет 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-Like


Во многих 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);

Сравнение для увеличения


Мы можем видеть параллели для повышения:: 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 open/read/close. Это связано с тем, что как basic_filebuf, так и FILE содержат внутренний буфер, который будет распределяться через стандартный распределитель (и, следовательно, не стирается из памяти).

Это можно легко сделать, изменив 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 использует буферизованные чтения и поэтому не должна использоваться.

Я также хотел бы поблагодарить Питера Кордеса за упоминание getrandom и отсутствие OpenBSD /dev/urandom.

Ответ 2

В некотором смысле это невозможно сделать портативно. То есть, можно представить действительную полностью детерминированную платформу, на которой запущен С++ (скажем, симулятор, который детерминирует шаговые машинные часы и "детерминированный" ввод-вывод), в которых нет источника случайности для извлечения PRNG.

Ответ 3

Вы можете использовать std::seed_seq и заполнить его, по крайней мере, до требуемого размера состояния генератора, используя метод Александра Хуссага для получения энтропии:

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 для правильного заполнения, было бы намного проще.

Ответ 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, несмотря на то, что они легко доступны из операционной системы. Поэтому я считаю, что это ошибка, и, скорее всего, это обычное явление для реализаций и платформ.

Ответ 5

Нет ничего плохого в посеве, используя время, предполагая, что вам не нужно, чтобы оно было безопасным (и вы не сказали, что это было необходимо). Понимание заключается в том, что вы можете использовать хеширование для исправления неслучайности. Я нашел, что это работает адекватно во всех случаях, в том числе и в частности для тяжелых симуляций Монте-Карло.

Одна из приятных особенностей этого подхода заключается в том, что он обобщает на инициализацию из других не-действительно случайных множеств семян. Например, если вы хотите, чтобы каждый поток имел свой собственный RNG (для обеспечения безопасности потоков), вы можете просто инициализировать на основе хешированного идентификатора потока.

Ниже приведена 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);
}

Ответ 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. Если хотя бы один раз источник "хорош", результат будет, по крайней мере, "хорошим".

Этот ответ не так короток, как было бы предпочтительнее, и может содержать одну или несколько ошибок логики. Поэтому я рассматриваю это как выполненную работу. Прокомментируйте, если у вас есть обратная связь.

Ответ 7

  • Используйте getentropy() для заполнения генератора псевдослучайных чисел (PRNG).
  • Используйте getrandom(), если вам нужны случайные значения (например, вместо /dev/urandom или /dev/random).

Они доступны в современных UNIX-подобных системах, таких как Linux, Solaris и OpenBSD.

Ответ 8

У данной платформы может быть источник энтропии, например /dev/random. Наносекунды с эпохи с std::chrono::high_resolution_clock::now(), вероятно, являются лучшим семенем в стандартной библиотеке.

Я ранее использовал что-то вроде (uint64_t)( time(NULL)*CLOCKS_PER_SEC + clock() ), чтобы получить больше бит энтропии для приложений, которые являются критическими для безопасности.