Skip to content

Repository files navigation

cpp_prngs

Build and test

When generating random numbers for games, the priorities are usually speed, reproducibility, portability, and convenience - not cryptographic security. The C and C++ standard facilities are often an awkward fit for those goals.

The classic C srand() / rand() interface is explicitly described by the C++ standard as a low-quality, non-portable and a source of possible data races. It relies on hidden global state: srand() changes the sequence used by rand(), and any part of a program can call either function. This makes random behavior hard to isolate and reproduce. Its underlying algorithm is unspecified, so the same seed may produce different results on different platforms. RAND_MAX is permitted to be as low as 32,767, and common attempts to turn its output into a useful range - such as rand() % n - are slow and can introduce modulo bias.

C++11 introduced <random>, which is much better, but it still has several practical drawbacks for game development:

  • Seeding the built-in engines correctly is notoriously difficult. Supplying enough high-quality seed material is easy to get wrong, and awkward enough to have motivated multiple C++ committee proposals, including P0205R1 and P0347R1. For games, this makes it harder than it should be to create reliable deterministic runs, procedural worlds and replays.

  • Standard distributions are not portable across standard-library implementations. Given the same engine state, distributions such as std::normal_distribution are not required to produce the same sequence of values on different platforms. That means a procedural level, simulation, or replay can diverge between Windows, Linux, consoles, or different compiler libraries even when the seed is identical. See P2059R0: Make Pseudo-random Numbers Portable.

  • The standard random facilities cannot currently run at compile time. Engines and distributions in <random> are not constexpr, so they cannot be used to generate lookup tables, test data, procedural content, or other random-derived values during compilation. Making the deterministic <random> facilities constexpr is still only proposed for C++29 in P3791R1.

The most widely used general-purpose engine in the C++ standard library is probably the Mersenne Twister, std::mt19937. It is a respectable generator, but it requires 624 state words - typically around 2.5 KiB of internal state. All else being equal, a large state puts more pressure on CPU caches. Modern generators such as xoshiro256**, PCG, and Romu require much less state and offer significantly better performance. The engines included here range from 4 bytes of state for tiny microcontroller-oriented generators to 32 bytes for general-purpose 64-bit generators.

For a deep, game-focused comparison of 47 PRNGs across nine platforms, see Rhet Butler's excellent RNG Battle Royale (2020). It compares the performance, portability, state size, and statistical quality that matter in real-world game development. Several of its top-performing generators - including Romu and SmallFast - are included here.

So, if you want a random-number generator that is:

  • compact (4–32 bytes of state) and fast
  • deterministic across platforms for supported fixed-width integer operations (and for floating-point operations when the representation and arithmetic behavior match)
  • easy to seed
  • feature-rich, with integers, floats, coin flips, weighted draws, random element selection, Gaussian samples, raw bits
  • usable at compile time with constexpr
  • compatible with STL algorithms and distributions such as std::shuffle, std::sample, and std::*_distribution

…go ahead and copy the complete include/ directory into your project, and go forth and prosper. Let me know if you find bugs or add any cool new features!

Try it on Compiler Explorer!


Getting Started

cpp_prngs is header-only. Copy the complete include/ directory into your project, add it to your compiler's include path, and include <rnd/random.hpp>. The same C++17 implementation is used on desktop and classic AVR-based Arduino targets; a private compatibility layer selects standard-library facilities or small fallbacks according to what the toolchain provides.

Desktop and full standard-library targets

Choose an engine and wrap it in Random<E>:

#include <rnd/engines/romuduojr.hpp> // The engine; choose another from include/rnd/engines if you prefer.
#include <rnd/random.hpp>            // The portable C++17 Random<E> frontend.

rnd::Random<rnd::RomuDuoJr> rng{1234}; // A generator with a fixed seed.
int damage = rng.between(10, 20); // Random integer in [10, 20).

Use Random<E> to access convenient utilities while keeping the engine easy to replace.

Arduino AVR

On a classic AVR-based Arduino, include the same header and consider one of the small-output engines:

#include <rnd/engines/small_fast16.hpp>
#include <rnd/random.hpp>

rnd::Random<rnd::SmallFast16> rng{1234};

const uint16_t blink_ms = rng.between(uint16_t{100}, uint16_t{500});
const bool turn_left = rng.coin_flip();

The Arduino AVR core defaults to C++11, so make sure to compile your sketch as C++17. See Building for Arduino AVR for a complete Arduino CLI command.

Weighted draws

Pass a collection of weights to weighted_index() when the weights themselves form the lookup table. Each returned index corresponds to the weight at that index:

#include <array>

constexpr std::array weights{50u, 30u, 15u, 5u};
const std::size_t tier = rng.weighted_index(weights);
// tier 0 is selected with weight 50, tier 1 with weight 30, and so on.

When weights are stored in a collection of objects, pass a projection to weighted_element() or weighted_iterator(). A pointer to the weight member is often all the projection you need:

#include <array>
#include <string_view>

struct LootDrop{
    std::string_view name;
    unsigned weight;
};

constexpr std::array loot_table{
    LootDrop{"potion", 50u},
    LootDrop{"gold", 30u},
    LootDrop{"magic sword", 15u},
    LootDrop{"dragon egg", 5u}
};

const LootDrop& drop = rng.weighted_element(loot_table, &LootDrop::weight);

Weights should be non-negative whole numbers. A weight of 0 means the item will never be selected. At least one weight must be greater than zero.

Try it on Compiler Explorer!


All included engines are header-only, C++17-compatible, usable during constant evaluation, and very fast. Their state ranges from 4 to 32 bytes.

General-purpose engines

These are the normal choices for desktop applications and other targets with efficient 32- or 64-bit arithmetic. Prefer a 64-bit-output engine on desktop unless you have a reason to choose otherwise.

Engine Output State Description
PCG32 32 bits 16 bytes C++ port of Melissa O’Neill’s minimal PCG32.
SmallFast32 32 bits 16 bytes C++ port of Bob Jenkins’ 32-bit Small Fast generator.
RomuDuoJr 64 bits 16 bytes C++ port of Mark Overton’s RomuDuoJr. Winner of Rhet Butler’s RNG Battle Royale (2020) and second fastest engine in the lineup!
QuarkBurst64 64 bits 24 bytes C++ port of Eightomic’s quarkburst1x64, previously published as GhostScramble. The fastest engine in the current benchmarks.
Konadare192 64 bits 24 bytes C++ port of Pelle Evensen's konadare192px++; the third-fastest 64-bit engine in the current benchmarks.
SmallFast64 64 bits 32 bytes A 64-bit three-rotate Small Fast implementation, using rotates (7, 13, 37).
Xoshiro256SS 64 bits 32 bytes C++ port of David Blackman and Sebastiano Vigna's xoshiro256** 1.0 generator.

Small-output engines for microcontrollers

These engines return 8 or 16 bits at a time and use only 4–8 bytes of state. Their narrow arithmetic can be a better fit for small microcontrollers such as AVR-based Arduino boards.

Engine Output State Description
SmallFast8 8 bits 4 bytes The smallest Small Fast variant, when every byte of state matters.
XorShift32Star8 8 bits 4 bytes A tiny xorshift* variant: Marsaglia's full-period 32-bit xorshift recurrence with Vigna-style multiplicative scrambling, returning the high 8 bits.
SmallFast16 16 bits 8 bytes A useful middle ground when an 8-bit result is too restrictive; uses O’Neill’s tested 16-bit constants.

An engine's output width only describes how many random bits it produces per draw; it does not limit the ranges supported by Random. When a wider value is needed, the library automatically combines multiple engine outputs, while small bounds continue to use the narrowest efficient representation. For example, an 8-bit engine can still generate a value from a 32- or 64-bit range, and bits_as() can explicitly fill any supported unsigned integer type with random bits.

Each included engine is a small, self-contained random number generator. You can use an engine directly, but it deliberately provides only the basics: seeding, advancing its state, comparing states, and generating random unsigned integers.

For everyday use, wrap an engine in Random<E>. It adds bounded numbers, floats, coin flips, random elements, weighted selection, approximate normal samples, and more while letting you swap the underlying engine without changing the rest of your code.

All included engines satisfy the C++20 rnd::RandomBitEngine concept and are compatible with standard C++ facilities such as std::shuffle and std::sample. Since porting to C++17 the rnd::Random<E> implementation checks its essential engine assumptions with static_assert diagnostics instead of requiring concepts.

Want to use your own engine? It must provide the interface described by rnd::RandomBitEngine, use an 8-, 16-, 32-, or 64-bit unsigned result_type, and span that type from zero through its maximum value.


Random API

rnd/random.hpp exposes rnd::Random<E> on every supported target.

Construction and engine state

Method Description
Random<E>() Default-constructs the engine E with its default seed
Random<E>(seed) Constructs by seeding the engine with seed
Random<E>(engine) Constructs by copying an existing engine instance
operator==(other) Returns true if two generators have identical state
engine() / engine() const Enables engine-specific operations and diagnostics; use the engine's state() API for portable snapshots.
engine().state() Returns the engine's complete public state aggregate for checkpointing or replay.
E::from_state(state) Reconstructs an engine from a complete state snapshot. Restoration validates engine invariants in debug builds.
seed() Reseeds the engine back to its default state
seed(v) Reseeds the engine with value v
discard(n) Advances the underlying engine by n steps
child() Derives a deterministic child generator from the parent by consuming enough output to fill one seed; it does not promise independent or non-overlapping streams

Portable state snapshots and replay

Every included engine exposes a public aggregate state_type, a state() snapshot, and a matching E::from_state(snapshot) factory. The aggregate contains the complete logical state, so a replay can resume without accessing private engine members:

using Engine = rnd::PCG32;
using Rng = rnd::Random<Engine>;

Rng live{123};
live.discard(100);
const Engine::state_type checkpoint = live.engine().state();

// Store checkpoint's numeric fields, then restore it later.
Rng replay{Engine::from_state(checkpoint)};

Restoring a state must satisfy the engine's invariants; all-zero states are invalid for the SmallFast engines, XorShift32Star8, RomuDuoJr, Konadare192, and Xoshiro256SS, while PCG32::state_type::inc must be odd.

The state fields are:

Engine Numeric state fields
SmallFast8, SmallFast16, SmallFast32, SmallFast64 a, b, c, d
XorShift32Star8 state
RomuDuoJr x, y
Konadare192, QuarkBurst64 a, b, c
PCG32 state, inc
Xoshiro256SS s0, s1, s2, s3

Raw values and bits

Method Description
min() Returns the engine’s minimum possible value, typically 0
max() Returns the engine’s maximum possible value
next() / operator()() Returns the next raw engine number in [min(), max()]; its width is result_type
bits(n) Returns n random bits in the low bits of T at runtime (1 ≤ n ≤ digits(T)), drawing from the high bits of one or more engine outputs; T defaults to result_type1
bits<N, T>() Returns N random bits in the low bits of T; constraints are checked at compile time1
bits_as<T>() Returns an unsigned T filled with high-quality random bits
fill_bits<T>(buffer, count) Efficiently fills buffer with raw random T values, minimizing engine calls when T is narrower than the engine output

Integers

Method Description
next(U bound) / operator()(U bound) For a fixed-width unsigned U, returns an unbiased U in [0, bound); reduction uses the smallest supported width that is at least the engine width and can represent bound
next<N, T>() Returns an integer in [0, N) with a compile-time bound and optional result type T; the reduction uses T's unsigned width and is optimized for power-of-two bounds1
between(I lo, I hi) Returns an integer in [lo, hi) using the unsigned width of I

Unbiased bounded integers

Turning a random engine value into a smaller range is slightly trickier than it first appears. An 8-bit engine, for example, has 256 possible outputs (0..255). If we ask for next(10), those 256 values cannot be divided evenly among 10 results: 256 = 25 * 10 + 6. A naive mapping therefore makes six results slightly more likely than the other four. This is range-reduction bias.

cpp_prngs uses Daniel Lemire's multiply-and-reject method. A random value is multiplied by the requested bound; the upper half of that product gives the result in [0, bound), while the lower half tells us whether the draw landed in the small leftover region responsible for the bias. Those few values are rejected and redrawn, giving every possible result equal probability.

Lemire's runtime algorithm is nearly divisionless: it first checks whether rejection is even possible, and only then computes the relatively expensive rejection cutoff. Tony Finch points out a useful specialization when the bound is known at compile time: the cutoff can then be computed during compilation, leaving the generated code division-free. next<Bound>() uses this form; power-of-two bounds are simpler still and reduce directly to random bit extraction.

Floating point

Method Description
normalized<F>() Returns a floating-point value in [0.0, 1.0) using the IQ float hack; F defaults to float
signed_norm<F>() Returns a floating-point value in [-1.0, 1.0); F defaults to float
between(F lo, F hi) Returns a floating-point value in [lo, hi)

Probability and distributions

Method Description
coin_flip() Fair coin flip (true approximately 50% of the time)
coin_flip(p) Weighted coin (true with probability p, where p is in [0.0, 1.0])
normal_approx(mean, stddev) Returns an approximate normal sample via the Irwin–Hall sum-of-12 method; support is roughly six standard deviations on either side of mean

Collections

Method Description
index(collection) Returns a random index into a collection; size_t collection sizes are supported even with narrow engines
iterator(collection) Returns the collection's iterator to a random element
element(collection) Returns a reference to a random element

iterator(container) returns the container's native iterator. iterator(pointer, count) returns a pointer to the selected element, which serves as the iterator for a pointer-defined range.

Weighted collections

Method Description
weighted_index(weights) Returns an index selected proportionally to unsigned weights; zero-weight indices are excluded
weighted_iterator(collection, projection) Returns the collection's iterator selected proportionally to weights returned by projection(element)
weighted_element(collection, projection) Returns a reference selected proportionally to weights returned by projection(element)

The weighted helpers let you pick items with different chances of being selected. Weights should be non-negative fixed-width unsigned integers, such as {70u, 25u, 5u}. A weight of 0 means the item will never be selected. At least one weight must be greater than zero, and the checked sum of all weights must fit in uint64_t; narrow engines gather enough bits only when the total requires a wider reduction width.

Portability and floating-point details

random.hpp uses standard type traits, collection access helpers, and constexpr std::bit_cast when the toolchain provides them. Its private compatibility layer also provides a narrow constexpr projection helper for callables, member functions, and data members. On AVR-libc, the layer supplies small fallbacks for the unavailable standard-library facilities.

In C++20 and later, normalized floating-point generation uses constexpr std::bit_cast. In C++17 it uses a constexpr arithmetic implementation by default. If you don't need constexpr execution you can define RND_FAST_FLOAT to select a runtime memcpy bit cast instead; only the floating-point methods lose constexpr evaluation in that mode.

Supported signed integer ranges, including ranges that cross zero or begin at the signed minimum, are reconstructed with defined C++17 arithmetic. Their results therefore do not depend on an implementation-defined unsigned-to-signed conversion. Floating-point reproducibility remains conditional on equivalent floating-point representations and arithmetic behavior.

Methods are templates or inline functions, so unused features do not add code to the final program.


Performance Benchmarks

The benchmark suite uses Quick Bench to measure three representative workloads:

The bounded benchmarks exercise the public Random<E> interface and compare it with equivalent C and C++ standard-library approaches. Lower bars are faster.

Engine throughput

Generating raw random numbers using next():

Engine throughput benchmark

All of the included engines substantially outperform the standard-library generators in this benchmark.

Bounded integers

Generating random integers in [0, bound) using Random<E>::next(bound):

Bounded integer benchmark

Bounded floating-point values

Generating random floats in [lo, hi) using Random<E>::between(lo, hi):

Bounded floating-point benchmark

The bounded benchmarks show that the convenience provided by Random<E> does not come at the expense of performance. QuarkBurst64, RomuDuoJr, and Konadare192 are currently the fastest engines in these comparisons.

Performance depends on the compiler, standard library, build settings, CPU, and workload. Always benchmark on your own target hardware before choosing an engine.


Seeding

All engines in this library are seeded from a single uint64_t value. They provide a fixed default seed, so default construction (Random<E>()) is always valid - but produces the same sequence every time.

To get varied sequences, you’ll want to provide a high-entropy seed. std::random_device is often used for this - it's typically backed by an operating-system entropy source and works fine on most platforms. But it can be slow, unavailable (e.g. on embedded systems, or at compile time), and is unsuitable when you need determinism.

In game development, determinism is often useful - for example in procedural generation, tests, or replays. In these cases, consistent seeds let you reproduce the same output across runs and platforms.

An optional standalone seeding.hpp is available separately from this repository. It is a collection of example seeding techniques for runtime and compile-time contexts, using sources such as timestamps, thread IDs, game assets, player data, and compilation metadata.

#include "seeding.hpp" // Download from the linked gist

//Example usage:
using rnd::Random; 

// Compile-time seeding:
constexpr auto seed1 = seed::from_text("my_game_seed");
constexpr auto seed2 = seed::from_source();           // Different for each compilation unit (source file)
constexpr auto seed3 = SEED_UNIQUE_FROM_SOURCE();     // Different for each macro expansion, even within the same source file

// Runtime seeding:
rnd::Random<rnd::SmallFast32> rng1(seed::from_time());		  // High resolution clock
rnd::Random<rnd::SmallFast64> rng2(seed::from_system_entropy());// Uses std::random_device (hardware/system entropy)

// Sources of run-to-run variation:
rnd::Random<rnd::RomuDuoJr> rng3(seed::from_thread());          // Unique per thread
rnd::Random<rnd::PCG32>     rng4(seed::from_stack());			  // Varies per run of the application, if ASLR is active
rnd::Random<rnd::PCG32>     rng5(seed::from_cpu_time());		  // Varies with CPU time consumed by the process; can reflect workload or scheduling

// Combine all available sources:
rnd::Random<rnd::Xoshiro256SS> rng6(seed::from_all());          // Combines all sources (time, thread, stack, heap, compile time, source data, hw entropy, etc.)

These utilities help you seed your random number generators appropriately - whether you need compile-time evaluation, reproducibility, run-to-run variation, or unpredictability.


Building and testing

CMake

The CMake build requires CMake 3.21 or newer. The repository provides a header-only target named cpp_prngs::cpp_prngs. When using cpp_prngs as a subdirectory, link that target to inherit its include path and C++17 requirement:

add_subdirectory(path/to/cpp_prngs)
target_link_libraries(your_target PRIVATE cpp_prngs::cpp_prngs)

To configure, build, and run the test suite directly:

cmake -S . -B build -DCPP_PRNGS_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
ctest --test-dir build -C Release --output-on-failure

The test build downloads the pinned GoogleTest dependency automatically. A shared development preset is also available:

cmake --preset dev
cmake --build --preset dev
ctest --preset dev

The test suite also builds the engines and both random.hpp floating-point modes as C++17 targets.

Building for Arduino AVR

The Arduino AVR core defaults to C++11, while the engines and random.hpp require C++17. The repository's CI validates an ATmega32U4 target with Arduino AVR core 1.8.8.

Arduino CLI

You can compile a sketch with C++17 through Arduino CLI:

arduino-cli core update-index
arduino-cli core install arduino:avr@1.8.8
arduino-cli compile \
  --fqbn arduino:avr:leonardo \
  --warnings all \
  --build-property "compiler.cpp.extra_flags=-std=gnu++17 -I/path/to/cpp_prngs/include" \
  /path/to/your/sketch

Replace /path/to/cpp_prngs/include with the repository's include directory and /path/to/your/sketch with your sketch directory. To enable the optional runtime-optimized floating-point path, add -DRND_FAST_FLOAT to compiler.cpp.extra_flags. CI compiles both modes.

Arduino IDE 2 on Windows

With Arduino IDE 2 on Windows, locate the installed AVR core under:

C:\Users\<username>\AppData\Local\Arduino15\packages\arduino\hardware\avr\<version>\

Create a platform.local.txt file next to platform.txt containing:

compiler.cpp.extra_flags=-std=gnu++17

Then restart the Arduino IDE.


License

This repository is primarily licensed under the MIT License. See LICENSE for full details.

Attributions and Third-Party Code

This project includes, or is based on, the following PRNG engines and reference implementations:

Where applicable, copyright and license information is included in the header of each source file.

All additional code, wrappers, and modifications © Ulf Benjaminsson, licensed under the MIT License unless otherwise noted.

Footnotes

  1. Although bits(n) and bits<N>() can be used for power-of-two integer ranges, this is not their intended purpose. Prefer next<N,T>() instead. It chooses the same fast, unbiased bit-shift specialization, but makes your code clearer and safer. ↩ ↩2 ↩3

About

Pseudo Random Number Generators for C++ game developers

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages