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)

그러나이 모든 것은 새로운 방식에 해당됩니다. 단지 더 빛나는 베니어를 가지고 있습니다.

  • rd()단일 unsigned int. 이것은 적어도 16 비트와 아마 32 비트를 가지고 있습니다. 이것은 MT의 19937 비트 상태를 시드하기에 충분하지 않습니다.

  • 사용 std::mt19937 gen(rd());gen()(32 비트로 시드하고 첫 번째 출력 확인)은 좋은 출력 분포를 제공하지 않습니다. 7과 13은 첫 번째 출력이 될 수 없습니다. 두 개의 씨앗이 0을 생산합니다. 12 개의 씨앗이 1226181350을 생산합니다. ( 링크 )

  • std::random_device고정 된 시드가있는 간단한 PRNG로 구현 될 수 있으며 때로는 구현됩니다. 따라서 모든 실행에서 동일한 시퀀스를 생성 할 수 있습니다. ( 링크 ) 이것보다 더 나쁘다 time(NULL).

더 나쁜 것은 앞서 언급 한 코드 조각이 포함 된 문제에도 불구하고 복사하여 붙여 넣기가 매우 쉽다는 것입니다. 이에 대한 일부 솔루션 은 모든 사람에게 적합하지 않을 수있는 거대한 라이브러리 를 확보 해야합니다.

이에 비추어 내 질문은 어떻게 C ++에서 mt19937 PRNG를 간결하고 이식 가능하며 철저하게 시드 할 수 있습니까?

위의 문제를 감안할 때 좋은 대답 :

  • mt19937 / mt19937_64를 완전히 시드해야합니다.
  • 엔트로피 에만 의존 std::random_device하거나 time(NULL)소스로 의존 할 수 없습니다 .
  • Boost 또는 다른 라이브러리에 의존해서는 안됩니다.
  • 답변에 복사하여 붙여 넣으면 멋지게 보이도록 적은 수의 줄에 맞아야합니다.

생각

  • 내 현재 생각은 , 주소 공간 무작위 화 에서 파생 된 값 및 하드 코딩 된 상수 (배포 중에 설정할 수 있음 std::random_device)로 출력을 매시업 (아마도 XOR을 통해)하여 엔트로피에서 최선을 다하는 샷을 얻을 수 있다는 것입니다.time(NULL)

  • std::random_device::entropy() 무엇을할지 안할지에 대한 좋은 지표를 제공 하지 않습니다std::random_device .



답변

가장 큰 결함 std::random_device은 CSPRNG를 사용할 수없는 경우 결정 론적 폴 백이 허용된다는 것입니다. 이것은 std::random_device생성 된 바이트가 결정적 일 수 있으므로를 사용하여 PRNG를 시드하지 않는 좋은 이유 입니다. 안타깝게도 이것이 언제 발생하는지 알아 내거나 저품질 난수 대신 실패를 요청하는 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;
}

유닉스 유사


많은 유닉스 계열 시스템에서 가능하면 / 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_PROVWindows에서 사용 하며 PROV_RSA_FULL. 빠진 유일한 것은 암호화 컨텍스트를 확인하는 것입니다 CRYPT_VERIFYCONTEXT. * Nix에서 Boost는 /dev/urandom. IE,이 솔루션은 이식 가능하고 잘 테스트되었으며 사용하기 쉽습니다.

Linux 전문화


보안을 위해 간결함을 희생하려는 경우 getrandomLinux 3.17 이상 및 최신 Solaris에서 탁월한 선택입니다. 커널이 부팅 후 아직 CSPRNG를 초기화하지 않은 경우 차단된다는 점을 제외하면 getrandom과 동일하게 작동 /dev/urandom합니다. 다음 스 니펫은 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;
}

감사


FILE버퍼링 된 읽기 사용 을 지적한 Ben Voigt에게 특별히 감사드립니다 . 따라서 사용해서는 안됩니다.

나는 또한 언급 한 Peter Cordes getrandom와 OpenBSD의 /dev/urandom.


답변

어떤 의미에서 이것은 이식 가능하게 할 수 없습니다. 즉, PRNG를 시드 할 임의의 소스가없는 C ++ (예 : 기계 시계를 결정적으로 단계적으로 이동하고 “결정된”I / O를 사용하는 시뮬레이터)를 실행하는 유효한 완전 결정적 플랫폼을 생각할 수 있습니다.


답변

a를 사용하고 std::seed_seqAlexander 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);
}

표준 라이브러리 의 UniformRandomBitGenerator 에서 SeedSequence 를 채우거나 만드는 적절한 방법이 있다면 적절하게 시드를 사용 하는 것이 훨씬 간단 할 것입니다.std::random_device


답변

내가 작업중인 구현 state_sizemt19937PRNG 의 속성을 활용 하여 초기화시 제공 할 시드 수를 결정합니다.

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


답변

보안이 필요하지 않다고 가정하면 시간을 사용하여 시딩하는 데 아무런 문제가 없습니다 (그리고 이것이 필요하다고 말하지 않았 음). 통찰력은 해싱을 사용하여 비 무작위성을 수정할 수 있다는 것입니다. 나는 이것이 무거운 Monte Carlo 시뮬레이션을 포함한 모든 경우에 적절하게 작동한다는 것을 발견했습니다.

이 접근 방식의 한 가지 좋은 특징은 실제로 무작위가 아닌 다른 시드 세트에서 초기화로 일반화된다는 것입니다. 예를 들어 각 스레드가 자체 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);
}


답변

질문에 대한 내 자신의 찌르기입니다.

#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. 최소한 한 번의 소스가 “좋음”이면 결과는 최소한 “좋음”이됩니다.

이 대답은 바람직한 것만 큼 짧지 않으며 논리 오류가 하나 이상 포함될 수 있습니다. 그래서 나는 그것을 진행중인 작업이라고 생각하고 있습니다. 피드백이 있으면 의견을 남겨주세요.


답변

  • getentropy ()를 사용하여 PRNG (pseudorandom number generator)를 시드합니다.
  • /dev/urandom또는 대신 임의의 값을 원한다면 getrandom ()을 사용하십시오 /dev/random.

Linux, Solaris 및 OpenBSD와 같은 최신 UNIX 계열 시스템에서 사용할 수 있습니다.