1

I am attempting to build a simple random number generator, but I wanted to make sure random_device was working properly. I started with the following code:

#include <random>
#include <chrono>

class Generator {
public:
    Generator()
        :
        m_DeviceSeed(rd()),
        m_TimeSeed(std::chrono::high_resolution_clock::now().time_since_epoch().count()),
        rng(m_DeviceSeed)
    {
        if (rd.entropy() == 0.0) {
            rng.seed((unsigned)m_TimeSeed);
        }
    }
private:
    //Vars
    std::random_device rd;
    unsigned int m_DeviceSeed;
    unsigned long long m_TimeSeed;
    std::mt19937 rng;
};

I had seen "std::chrono::high_resolution_clock::now().time_since_epoch().count()" recommended as an alternative to random_device, and I figured checking entropy would allow me to use it as a fallback; however, this is written in Visual Studio, and apparently that means entropy always shows 32, regardless of if it is true or not.

So, my question is this: what is the most robust way to seed std::mt19937 without a means of testing entropy? Is chrono better, or random_device? Or some combination, or other option entirely?

Based off of this: The implementation of random_device in VS2010?

It seems random_device is a safe pick for seeding or generating seed_sequences in most situations, but I want to be sure.

Jatol
  • 51
  • 4
  • Use the current epoch? – NathanOliver Sep 03 '20 at 16:30
  • If you can't rely on the entropy, then don't use it. Your backup plan should be good enough for using in all cases. – Mark Ransom Sep 03 '20 at 16:31
  • Note that in the constructor initializer list item for `m_DeviceSeed`, `rd` has not been constructed, so `rd()` does not necessarily do anything meaningful. – Pete Becker Sep 03 '20 at 16:41
  • Keeping `m_DeviceSeed`, `m_TimeSeed`, and `rd` around even though they are only used once, at most, seems pointless. Write a function to calculate your seed; your function can create a local object `rd` and return either `rd()` or some time-based seed. Now the initializer list consist only of `rng(get_seed())`. – Pete Becker Sep 03 '20 at 16:46
  • 2
    "So, my question is this: what is the most robust way to seed std::mt19937 without a means of testing entropy?" - I would go with seeding with a [std::seed_seq](https://en.cppreference.com/w/cpp/numeric/random/seed_seq) that is constructed with some fairly random things like current time (with as much precision as possible), the current process ID, the address of `main` in memory, the user id of the current user, etc etc and then fill up with output from `std::random_device`. – Jesper Juhl Sep 03 '20 at 17:06
  • 2
    One way would be to write your code correctly. To whit: `m_DeviceSeed(rd())` This is UB. `rd` *has not been initialized yet*, because members are initialized in declaration order. – Nicol Bolas Sep 03 '20 at 17:38
  • Hello All, and thank you for your replies! Apologies for not declaring in the right order to initialize correctly. Also, worth mentioning the reason I saved the seeds in the example is because the full code I was testing with occasionally accessed seeds. Thank you for all of your advice, though! I will see about creating a get_seed() style function using a combination of sources! – Jatol Sep 03 '20 at 21:39

1 Answers1

2

I took some time after work to dig into Microsoft documentation and the VS source, and I figured I should share! The header had this:

_NODISCARD double entropy() const noexcept
    {   // return entropy of random number source
    return (32.0);
    }

_NODISCARD result_type operator()()
    {   // return next value
    return (_Random_device());
    }

First part, looks like entropy is always 32.0 after all, and the operator calls _Random_Device. I was able to track that definition down to xrngdev.cpp, which used rand_s.

_CRTIMP2_PURE unsigned int __CLRCALL_PURE_OR_CDECL _Random_device()
    {   // return a random value
    unsigned int ans;
    if (_CSTD rand_s(&ans))
        _Xout_of_range("invalid random_device value");
    return (ans);
    }

As per Microsoft documentation, "The rand_s function writes a pseudorandom integer in the range 0 to UINT_MAX to the input pointer. The rand_s function uses the operating system to generate cryptographically secure random numbers."

Also, tracking down the rand_s.cpp showed this:

    if (!__acrt_RtlGenRandom(result, static_cast<ULONG>(sizeof(*result))))
{
    errno = ENOMEM;
    return errno;
}

Which confirms it uses RtlGenRandom, as seen here:The implementation of random_device in VS2010?

RtlGenRandom is sort of a black box, but from what has been released, it uses:

[RtlGenRandom] generates as specified in FIPS 186-2 appendix 3.1 with SHA-1 as the G function. And with entropy from:

The current process ID (GetCurrentProcessID).

The current thread ID (GetCurrentThreadID).

The tick count since boot time (GetTickCount).

The current time (GetLocalTime).

Various high-precision performance counters (QueryPerformanceCounter).

An MD4 hash of the user's environment block, which includes username, computer name, and search path. [...]

High-precision internal CPU counters, such as RDTSC, RDMSR, RDPMC

[omitted: long lists of low-level system information fields and performance counters] [4]

So yeah, std::random_device appears to be the most robust option on pretty much any Windows application post-WindowsXP.

While I will still follow the provided comments and create a get_seed() style function to generate a full std::seed_seq with some extra twists, this at least answers for me that std::random_device is likely the more robust "base" seeding option.

Jatol
  • 51
  • 4