UTL

Collection of self-contained header-only libraries for C++17

View on GitHub

utl::random

<- to README.md

<- to implementation.hpp

random module implements several additions to std <random> that aim to:

The module was created during my ongoing work on a thesis that uses stochastic differential equations and Monte—Carlo method to model explosive heat transfer in a turbulent flow.

It implements several random bit generators seamlessly compatible with <random>:

These pseudorandom number generators (aka PRNGs) cover most of the common uses cases better than somewhat outdated standard library PRNGs, see notes on random number generation.

Why use utl::random over built-in functions?

Definitions

// PRNG implementations
namespace generators {
    class GeneratorAPIExample {
        using result_type;
        
        static constexpr result_type min() noexcept;
        static constexpr result_type max() noexcept;
        
        constexpr GeneratorAPI(result_type seed) noexcept;
        constexpr void    seed(result_type seed) noexcept;
        
        template<class SeedSeq> GeneratorAPI(SeedSeq& seq);
        template<class SeedSeq> void    seed(SeedSeq& seq);
        
        constexpr result_type operator()() noexcept;
    };
    
    // 16-bit PRNGs
    class RomuMono16   { /* Generator API */ };
    // 32-bit PRNGs
    class RomuTrio32   { /* Generator API */ };
    class SplitMix32   { /* Generator API */ };
    class Xoshiro128PP { /* Generator API */ };
    // 64-bit PRNGs
    class RomuDuoJr64  { /* Generator API */ };
    class SplitMix64   { /* Generator API */ };
    class Xoshiro256PP { /* Generator API */ };
    // CSPRNGs
    class ChaCha8      { /* Generator API */ };
    class ChaCha12     { /* Generator API */ };
    class ChaCha20     { /* Generator API */ };
}

// Default global PRNG
using default_generator_type = generators::Xoshiro256PP;
using default_result_type    = std::uint64_t;

inline default_generator_type default_generator;

void seed(std::uint64_t seed) noexcept;
void seed_with_entropy();

// Entropy
std::seed_seq entropy_seq();
std::uint32_t entropy();

// Distributions
template <class T>
struct UniformIntDistribution  { /* same API as std::uniform_int_distribution<T> */  };

template <class T>
struct UniformRealDistribution { /* same API as std::uniform_real_distribution<T> */ };

template <class T, class Gen>
constexpr T generate_canonical(Gen& gen) noexcept(noexcept(gen()));

// Convenient random functions
int rand_int(int min, int max)                    noexcept;
int rand_uint(unsigned int min, unsigned int max) noexcept;

float rand_float()                     noexcept; // U[0, 1]     uniform distribution
float rand_float(float min, float max) noexcept; // U[min, max] uniform distribution
float rand_normal_float();                       // N(0, 1)      normal distribution

double rand_double()                       noexcept; // U[0, 1]     uniform distribution
double rand_double(double min, double max) noexcept; // U[min, max] uniform distribution
double rand_normal_float();                          // N(0, 1)      normal distribution

bool rand_bool() noexcept;

template<class T>
constexpr T& rand_choice(std::initializer_list<T> objects) noexcept;

template<class T>
T rand_linear_combination(const T& A, const T& B);

Methods

Random bit generators

class GeneratorAPIExample {
    using result_type;

    static constexpr result_type min() noexcept;
    static constexpr result_type max() noexcept;

    constexpr GeneratorAPI(result_type seed) noexcept;
    constexpr void    seed(result_type seed) noexcept;

    template<class SeedSeq> GeneratorAPI(SeedSeq& seq);
    template<class SeedSeq> void    seed(SeedSeq& seq);

    constexpr result_type operator()() noexcept;
};

// 16-bit PRNGs
class RomuMono16   { /* Generator API */ };
// 32-bit PRNGs
class RomuTrio32   { /* Generator API */ };
class SplitMix32   { /* Generator API */ };
class Xoshiro128PP { /* Generator API */ };
// 64-bit PRNGs
class RomuDuoJr64  { /* Generator API */ };
class SplitMix64   { /* Generator API */ };
class Xoshiro256PP { /* Generator API */ };
// CSPRNGs
class ChaCha8      { /* Generator API */ };
class ChaCha12     { /* Generator API */ };
class ChaCha20     { /* Generator API */ };

All of these generators satisfy uniform random bit generator generator requirements and std::uniform_random_bit_generator concept, which makes them drop-in replacements for standard generators such as std::mt19937.

Unlike standard generators these can also be used in constexpr functions.

Note: All provided PRNGs have min() == 0 and max() == std::numeric_limits<result_type>::max().

Default global PRNG

using default_generator_type = generators::Xoshiro256PP;
using default_result_type    = std::uint64_t;

Typedefs for default PRNG of this module and its return type.

inline default_generator_type default_generator;

A global instance of Xoshiro256++ generator used by convenience functions of this module.

Note: All random engines are inherently non-thread-safe, a proper way of generating numbers in parallel is to create local generators on each thread and seed them with different values.

void random::seed(std::uint64_t random_seed) noexcept;

Seeds global random engine with random_seed.

void random::seed_with_entropy();

Seeds global random engine using combined entropy from several sources, the main one being std::random_device which uses hardware source of non-deterministic randomness.

It is effectively the same as seeding global engine with random::entropy_seq().

Note 1: Resist the temptation to seed engines with std::time(NULL), using proper entropy is how it should be done.

Note 2: If no hardware randomness is available, std::random_device falls back onto an internal PRNG, it is generally not an issue due to multiple sources of entropy, however it makes cryptographic usage quite tricky.

Entropy

std::seed_seq entropy_seq();
std::uint32_t entropy();

These functions serve a role of a “slightly better and more convenient std::random_device”.

std::random_device has a critical deficiency in it’s design — in case its implementation doesn’t provide a proper source of entropy, it is free to fallback onto a regular PRNGs that don’t change from run to run. The method std::random_device::entropy() which should be able to detect that information is notoriously unreliable and returns different things on every platform.

entropy() samples several sources of entropy (including the std::random_device itself) and is (almost) guaranteed to change from run to run even if it can’t provide a proper hardware-sourced entropy that would be suitable for cryptography. It can be used as a drop-in replacement to std::random_device{}() calls.

entropy_seq() generates a full std::seed_seq instead of a single number, it is mainly useful for seeding generators with a large state.

Note: These functions are thread-safe.

Distributions

template <class T>
struct UniformIntDistribution {
    /* ... */
};

Uniform integer distribution class that provides a 1-to-1 copy of std::uniform_int_distribution API, except:

Note: This is a close reimplementation of std::uniform_int_distribution for GCC libstdc++ with some additional considerations, it provides similar performance and in some cases may even produce the same sequence.

template <class T>
struct UniformRealDistribution {
    /* ... */
};

Uniform floating-point distribution class that provides a 1-to-1 copy of std::uniform_real_distribution API, except:

Note: This is a close reimplementation of std::uniform_real_distribution for MSVC STL with some additional considerations and special logic for common optimizable cases.

How is it faster than std: Parts of std::generate_canonical<>() which is used by std::uniform_real_distribution can be moved to constexpr , avoiding the runtime std::log() computation. Some cases such as double & float generation with bit-uniform PRNGs (aka all PRNGs of this module and std::mt19937) can be handled with a simple bitmask rather than a generic implementation of std::generate_canonical<>() which accounts for all the esoteric PRNGs with weird ranges that could exist. This produces a speedup of up to 4 times. Combined with a speedup from faster PRNGs it is possible to achieve over 10 times faster random double generation in an interval.

template <class T, class Gen>
constexpr T generate_canonical(Gen& gen) noexcept(noexcept(gen()));

Generates a random floating point number in range $[0, 1]$ similarly to std::generate_canonical<>().

Always generates std::numeric_limits<T>::digits bits of randomness, which is enough to fill the mantissa. See UniformRealDistribution for notes on implementation improvements.

Convenient random functions

int random::rand_int(          int min,          int max);
int random::rand_uint(unsigned int min, unsigned int max);

Returns random integer in a $[min, max]$ range.

float  random::rand_float();
double random::rand_double();

Returns random float/double in a $[0, 1]$ range.

float  random::rand_float(  float min,  float max);
double random::rand_double(double min, double max);

Returns random float/double in a $[min, max]$ range.

float  random::rand_normal_float();
double random::rand_normal_double();

Returns random normally distributed float/double with a mean $0$ and variance $1$.

bool random::rand_bool();

Returns true/false randomly. Effectively same as rand_uint(0, 1).

const T& rand_choice(std::initializer_list<T> objects);

Returns randomly chosen object from a list.

T rand_linear_combination(const T& A, const T& B);

Returns $\alpha A + (1 - \alpha) B$, with random $0 \leq \alpha \leq 1$. Useful for vector and color operations. Object must have a defined operator+() and scalar operator*().

Examples

Getting random values

[ Run this code ]

using namespace utl;

random::seed_with_entropy();
std::cout
    << "rand_int(0, 100)                = " << random::rand_int(0, 100)                << "\n"
    << "rand_double()                   = " << random::rand_double()                   << "\n"
    << "rand_double(-5, 5)              = " << random::rand_double(-5, 5)              << "\n"
    << "rand_bool()                     = " << random::rand_bool()                     << "\n"
    << "rand_choice({1, 2, 3})          = " << random::rand_choice({1, 2, 3})          << "\n"
    << "rand_linear_combination(2., 3.) = " << random::rand_linear_combination(2., 3.) << "\n";

Output:

rand_int(0, 100)                = 14
rand_double()                   = 0.333702
rand_double(-5, 5)              = -1.9462
rand_bool()                     = 0
rand_choice({1, 2, 3})          = 2
rand_linear_combination(2., 3.) = 2.13217

Using custom PRNGs with <random>

[ Run this code ]

using namespace utl;

random::generators::SplitMix64 gen{random::entropy()};
std::chi_squared_distribution distr{2.}; // Chi-squared distribution with a n = 2

std::cout << "Random value from distribution -> " << distr(gen) << "\n";

Output:

Random value from distribution -> 4.80049

Constexpr random

[ Run this code ]

template <std::size_t size>
constexpr auto random_integers(std::uint64_t seed, int min, int max) {
    std::array<int, size> res{};
    utl::random::UniformIntDistribution dist{min, max};
    utl::random::default_generator_type gen(seed);
    for (auto &e : res) e = dist(gen);
    return res;
}

// ...

constexpr auto random_array = random_integers<6>(0, -10, 10);

utl::log::println(random_array); // using another module for convenience

// compile-time random like this can be used to automatically build lookup tables
// and generate seemingly random patterns

Output:

{ -2, 6, 6, 3, -6, 2 }

Notes on random number generation

As of 2025, the selection of pseudorandom number generators (aka PRNGs) in the standard library <random> is quite outdated, with most generators being developed before year 2000 and providing sub-par characteristics relative to what can be achieved.

While suitable for most uses cases, better performance & quality can be achieved virtually “for free” by switching to some of the newer PRNG implementations.

Thankfully, <random> design is quite flexible and fully abstracts the concept of a random bit generator, which makes it seamlessly compatible with any custom PRNG that exposes the necessary API.

utl::random provides <random>-compatible implementations of several modern PRNGs. By default, rand functions from this header use Xoshiro256++ as it well tested, used by several modern languages (Rust, Julia, slightly different version is used by .NET, GNU FORTRAN and Lua) as their default and provides an excellent balance of speed and statistical quality.

Overview of available PRNGs

Generator Performance Memory Result type Quality Period Motivation
RomuMono16 ~500% 4 bytes std::uint16_t ★★☆☆☆ $\approx 2^{32}$ Fastest 16-bit PRNG ⁽¹⁾
RomuTrio32 ~470% 12 bytes std::uint32_t ★★☆☆☆ Chaotic ⁽²⁾ Fastest 32-bit PRNG
SplitMix32 ~540% 4 bytes std::uint32_t ★★★☆☆ $2^{32}$ Smallest state 32-bit PRNG
Xoshiro128PP ~375% 16 bytes std::uint32_t ★★★★☆ $2^{128} − 1$ Best all purpose 32-bit PRNG
RomuDuoJr64 ~600% 16 bytes std::uint64_t ★★☆☆☆ Chaotic Fastest 64-bit PRNG
SplitMix64 ~540% 8 bytes std::uint64_t ★★★★☆ $2^{64}$ Smallest state 64-bit PRNG
Xoshiro256PP ~385% 32 bytes std::uint64_t ★★★★☆ $2^{256} − 1$ Best all purpose 64-bit PRNG
ChaCha8 ⁽³⁾ ~125% 120 bytes std::uint32_t ★★★★★ $2^{128}$ Cryptographically secure PRNG
ChaCha12 ~105% 120 bytes std::uint32_t ★★★★★ $2^{128}$ Cryptographically secure PRNG
ChaCha20 ~70% 120 bytes std::uint32_t ★★★★★ $2^{128}$ Cryptographically secure PRNG
std::minstd_rand 100% 8 bytes std::uint64_t ★☆☆☆☆ $2^{31} − 1$  
rand() ~80% Platform-dependent ⁽⁴⁾ int ★☆☆☆☆ Platform-dependent  
std::mt19937 ~105% 5000 bytes std::uint32_t ★★★☆☆ $2^{19937} − 1$  
std::knuth_b ~55% 2064 bytes std::uint64_t ★★☆☆☆ $2^{31} − 1$  
std::ranlux48 ~4% 120 bytes std::uint64_t ★★★★☆ $\approx 2^{576}$  

[!Important] Performance ratings are relative to the commonly used std::minstd_rand / rand(). Particular number may differ depending on the hardware and compilation settings, however general trends tend to stay the same. Benchmarks can be found here.

[!Important] Performance is measured in values per unit of time, to get a bytes per unit of time metric, the measurements can be normalized by a sizeof(result_type), making 32-bit generators effectively two times slower than listed in the table.

[!Note] (1) Here “fastest” also takes into account such things as register pressure and “friendliness” towards instruction-level parallelism, which is not always apparent on a heavily loaded benchmark.

[!Note] (2) Non-linear PRNGs also known as “chaotic PRNGs” have a cycle length that is different for each seed. This introduces a theoretical possibility of encountering short cycles, but opens up several avenues for optimization.

[!Note] (3) The difference between ChaCha8, ChaCha12 and ChaCha20 is the number of stream cypher rounds — 8, 12 and 20 correspondingly. More rounds make the state more difficult to discover, but have a negative effect on performance. As of now (year 2025) ChaCha12 seems like a reasonable default since it provides 5 rounds of security margin over the best known attack.

[!Note] (4) C function rand() is implementation-defined, on most existing implementation it uses an old LCG engine similar to std::minstd_rand. It is generally an extremely low-quality way of generating random and faces a host of additional issues on platforms with low RAND_MAX, which includes Windows where RAND_MAX is equal 32767 (less than 2 bytes of information, an almost ridiculous value, really). GCC, which is used in this benchmark, implements rand() using linear feedback shift register, which is less likely to encounter the most blatant of issues, but is still ultimately an inferior approach.

Random quality ratings are as follows:

Quality rating Usage Quality description
★★★★★ Suitable for cryptography Cryptographically secure, satisfies CSPRNG requirements, this usually comes at the price of performance
★★★★☆ Suitable for most use cases No significant issues in qualitative testing, passes TestU01 Big Crush
★★★☆☆ Suitable for most use cases No significant issues in qualitative testing, might fail a few tests on Big Crush
★★☆☆☆ Suitable for simple applications Significant flaws in statistical quality in certain aspects
★☆☆☆☆ Suitable for simple applications Significant flaws in statistical quality all-around

Why RNG quality matters

Using low-quality random may introduce artificial biases and unexpected effects into what should be a stochastic simulation. Below are some examples of such effects:

These issues become particularly dangerous and difficult to diagnose in multidimensional simulations, as a lot of older PRNGs have significant statistical issues in higher dimensions that simply aren’t apparent in a 1D case.

In general, most statistical issues fall into following groups:

Visual examples

RANDU

The most famous example of low-quality random is IBM’s RANDU (a variation of the same LCG used by most C programmers to this day) which was widely used in 1960s and 1970s for the lack of a better knowledge.

As a simplest showcase of its flaws, let’s generate 10000 random points in a $[0, 1]^3$ cube, each points will generate with coordinates { randu(), randu(), randu() }. After plotting the resulting picture and looking at it from a correct angle, we can clearly see that all points fall into just 12 distinct planes:

What we just observed is called “failing the spectral test at an extremely low dimension”, and for RANDU that dimension is 3. Such quality is clearly not something one would want in their Monte—Carlo simulation with “random” realizations.

In fact, this quality is generic to all LCGs — any LCG with modulus $m$ used to generate points in $N$-dimensional space will result in no more that $(N! \times m)^{1/N}$ hyperplanes, with other LCG implementations it’s just not as noticeable that one could see the planes visually.

minstd_rand & rand()

While RANDU algorithm is no longer in use today, its family of algorithms (LCG) is still frequently used through rand() and std::minstd_rand, with rand() being the worst offender as it is the default way of generating random numbers in C (C++ guides and documentation tend to prefer std::mt19937 which despite not being ideal avoids most of the significant issues).

Note: Since nothing in the standard specifies how rand() should be implemented it is generally not something one could rely upon, in most cases it’s LCG, sometimes it’s linear-feedback shift register which is a little better.

We can easily demonstrate a significant correlation of successive seeds in the default implementation by creating a $200 \times 160$ matrix of random 1s and 0s, while re-seeding the generator with i at the beginning of each row i. This will lead to a following image:

Using std::mt19937 (or any other even slightly sensible engine) will not lead to such an horrific correlation, we get a visually uniform image:

In practice, something like this would be encountered during any stochastic simulation where each run is simulated with a new seed.

Why not just use Mersenne Twister

One could argue that a widely used Mersenne Twister, which, in case of C++ is called std::mt19937 should be good enough for most purposes and that is true. For quite a while Mersenne Twister used to be the default choice for new RNG facilities — it is acceptably performant and has a decent statistical quality with a huge period.

However Mersenne Twister still fails some of the less obvious statistical tests on TestU01 BigCrush, combined with rather subpar performance relative to the newer methods and a huge state (5000 bytes against 32 used by Xoshiro256++) there is little reason to use it in a modern environment.

This trend can be observed rather clearly by looking at the lineup of default PRNGs used in different programming languages:

Language Fist release Default PRNG Quality
C 1972 LCG Bad
Matlab 1979 Mersenne Twister Decent
C++ 1985 Mersenne Twister / LCG Decent / Bad
Python 1991 Mersenne Twister Decent
GNU Octave 1993 Mersenne Twister Decent
Python NumPy 2006 Mersenne Twister (below v.1.17) ➔ PCG64 (above v.1.17) Decent ➔ Good
Julia 2012 Xoshiro256++ Good
Rust 2015 ChaCha12 / Xoshiro256++ Good
.NET 2016 Subtractive Generator (below v.6) ➔ Xoshiro256&ast;&ast; (above v.6) Decent ➔ Good

While older languages usually stick to their already existing implementations, newer project tend to choose modern PRNGs for the purpose. This establishes a strong case for switching to using Xoshiro / PCG family of PRNGs as a default choice in new projects. Engines of families such as ChaCha and ISAAC also provide cryptographic security to the random sequence, which in essence means that the state of the engine cannot be easily discovered from a piece of its random sequence. This usually has a noticeable performance cost, however even they fair rather well compared to the old monsters such as std::ranlux48 which runs almost 80 times slower than Xoshiro256++.

Additional considerations

Output range

In practice, most PRNG outputs aren’t used directly as they are generated but rather pass thorough an additional layer of abstraction such as, for example std::uniform_int_distribution, to compute a distributed value.

This has a noticeable effect on quality and performance of some PRNGs, for example, engines with ::min() != 0 or ::max() != std::numeric_limits<result_type>::max() are inherently at a disadvantage due to preventing the usage of Lemire’s algorithm for uniform integer distribution in an interval, which makes libstdc++ fallback onto a significantly slower generic algorithm, effectively making the PRNG benchmark misleading about real performance.

For this reason all generators selected for this module provide a full range from 0 to std::numeric_limits<result_type>::max() and try to avoid “surprises”.

Seeding

Some engines can be “picky” about zero-seeding or require a non-trivial seeding with std::seed_seq which is cumbersome and difficult to get right. This is exactly the case with widely std::mt19937 which is usually seeded like this:

std::random_device rd; // create source of entropy

std::mt19937 gen{rd()}; // seed with too little state

This is not nearly enough entropy for a huge state of std::mt19937, a “proper” way of seeding would be using more entropy and “spreading” it to the state using std::seed_seq:

std::array<std::mt19937::result_type, std::mt19937::state_size> state;

std::random_device rd; // create source of entropy

std::seed_seq seq({rd(), rd(), rd(), rd()}); // create seeding sequence
seq.generate(state.begin(), state.end());

std::mt19937 gen(seq); // seed mt19937 properly

Such approach however is beyond cumbersome and is rarely used in practice, which is why it is sensible to pick generators that work well with a single-number seeding as a default option.

Entropy

Unfortunately the is no “nice” and portable way of getting proper cryptographic entropy is a standard library. Main issues were already mentioned in the section documenting random::entropy_seq(), the best we can do without using system API is to sample everything we can (std::random_device, time in nanoseconds, address space, some other PRNG and etc.) and use seeding sequence to mash it all together into a single state.

The result of such approach is generally satisfactory, however oftentimes not sufficient for proper cryptographic security.