CS 70

The Source for Our randuint32 Library

This is bonus material. You don't need to look at this code.

Header File: randuint32.hpp

/* randuint32.hpp
   ==============

   Interface definition for the RandUInt32 class, which provides a simple
   way to get random numbers.

   This file is provided as part of CS 70, but may be used in other
   code.
*/

#ifndef RANDUINT32_HPP_INCLUDED
#define RANDUINT32_HPP_INCLUDED

#include <iostream>
#include <cstdint>
#include "jsf.hpp"

/**
 * \class RandUInt32
 * \brief Provides a simplified way to get random numbers.
 *
 * \details
 * Contains a C++-style a random number engine as a data member that it
 * uses to produce the numbers.
 *
 */

class RandUInt32 {
 public:
    /**
     * \brief Initializes a RandUInt32 object with a random seed from
     *        system entropy.
     */
    RandUInt32();

    /**
     * \brief Initializes a RandUInt32 object with user-provided seed.
     * \param seedval  the seed
     */
    RandUInt32(uint64_t seedval);

    /**
     * \brief Get a random value in the range [0,max) (i.e., not including
     *        max).
     * \param max  the past-the-end value
     */
    uint32_t get(size_t max);

    /**
     * \brief Get a 32-bit integer.
     * \warning Do not run `mygen.get() % range`, use `mygen.get(range)`
     *          instead.  The former is both slower and also biased.
     */
    uint32_t get();

 private:
    typedef jsf32 rng_type;  // Can be swapped out for another C++-style PRNG
    rng_type rng_;           // The size of this type depends on this data
                             // member.
};

#endif

The class is designed so that we could change rng_type to be a different PRNG. Currently it uses jsf32, but it could for example use std::minstd_rand or std::mt19937 instead.

Source File: randuint32.cpp

The following would be entirely sufficient:

/* randuint32.cpp
   ==============

   Implementation of the RandUInt32 class, which provides a simple way
   to get random numbers.

   This file is provided as part of CS 70, but may be used in other
   code.
*/

#include "randuint32.hpp"
#include <chrono>
#include <random>

/*
 * \brief Generates 64 bits of system entropy, based on the two calls to
 *        the random device and one to the high-resolution clock.
 *        64 bits is about the bare minimum for a generic random seed.
 *
 * \note  More about seeding can be found at
 *            https://www.pcg-random.org/categories/seeding.html
 */
static uint64_t systemEntropy() {
    // Set up 64-bits of randomness from the random device.
    std::random_device rdev;
    uint64_t pool = rdev();
    pool <<= 32;
    pool |= rdev();

    // Xor in more entropy from the system clock, in case std::random_device
    // doesn't actually produce system entropy (it's not required to!!).
    // See https://www.pcg-random.org/posts/cpps-random_device.html
    pool ^=
        std::chrono::high_resolution_clock::now().time_since_epoch().count();
    return pool;
}

RandUInt32::RandUInt32() : rng_(systemEntropy()) {
    // Nothing (else) to do.
}

RandUInt32::RandUInt32(uint64_t seedval) : rng_(seedval) {
    // Nothing (else) to do.
}

uint32_t RandUInt32::get(size_t max) {
    std::uniform_int_distribution<uint32_t> udist(0, max - 1);
    return udist(rng_);
}

uint32_t RandUInt32::get() {
    std::uniform_int_distribution<uint32_t> udist(0, UINT32_MAX);
    return udist(rng_);
}

If you want to have you're own version of RandUInt32 to use, the one above might be the best, because the code is the simplest.

For this class though, we like to give examples where we can know for sure what the output would be for a given seed. The std::uniform_int_distribution class template is not guaranteed to produce the same results on different standard library implementations (or even different versions of the same library). To have identical results across systems, we avoid using std::uniform_int_distribution and instead implement its functionality ourselves (unless you switch the underlying generator to something that makes doing that super-awkward).

Generally though, reimplementing code that is already in the standard library is a bad idea. Often considerable engineering effort has gone into making the standard library code perform very well (although obviously that cany vary from system to system).

The code below uses various recent C++ features we've not covered in this class, including templated (compile-time) constants, and if constexpr which is an if whose outcome must be determined at compile-time.

/* randuint32.cpp
   ==============

   Implementation of the RandUInt32 class, which provides a simple way
   to get random numbers.

   This file is provided as part of CS 70, but may be used in other
   code.
*/

#include "randuint32.hpp"
#include <chrono>
#include <random>
#include <limits>

/*
 * \brief Generates 64 bits of system entropy, based on the two calls to
 *        the random device and one to the high-resolution clock.
 *        64 bits is about the bare minimum for a generic random seed.
 *
 * \note  More about seeding can be found at
 *            https://www.pcg-random.org/categories/seeding.html
 */
static uint64_t systemEntropy() {
    // Set up 64-bits of randomness from the random device.
    std::random_device rdev;
    uint64_t pool = rdev();
    pool <<= 32;
    pool |= rdev();

    // Xor in more entropy from the system clock, in case std::random_device
    // doesn't actually produce system entropy (it's not required to!!).
    // See https://www.pcg-random.org/posts/cpps-random_device.html
    pool ^=
        std::chrono::high_resolution_clock::now().time_since_epoch().count();
    return pool;
}

/*
 * \brief C++ allows PRNGs to have completely arbitrary output ranges, for
 *        example, you could have one that outputs only 7 distinct values.
 *        This code provides a compile-time test to make sure the PRNG output
 *        uses the full range of the type and at least 32-bits.
 *        Modern PRNGs are all full-range.
 */
template <typename PRNG>
static constexpr bool is_fullwidth_prng =
    PRNG::min() == 0 && PRNG::max() >= std::numeric_limits<uint32_t>::max() &&
    PRNG::max() == ~(typename PRNG::result_type(0));

RandUInt32::RandUInt32() : rng_(systemEntropy()) {
    // Nothing (else) to do.
}

RandUInt32::RandUInt32(uint64_t seedval) : rng_(seedval) {
    // Nothing (else) to do.
}

uint32_t RandUInt32::get(size_t max) {
    // The downside of using std::uniform_int_distribution is that
    //   a) It's super generic, it can even work with a PRNG that only outputs
    //      random numbers in the range 42 to 56.  That makes it a bit slow.
    //   b) The implementation is up to the standard library, so different
    //      standard libraries produce different outputs.  For portable
    //      reproducibility, we need to write our own.
    // So we avoid it if we can.

    // static_assert(is_fullwidth_prng<rng_type>, "PRNG is not full-width");
    // ^-- uncomment this line to ensure only full-width PRNGs may be used

    if constexpr (is_fullwidth_prng<rng_type>) {
        // This code is rom https://www.pcg-random.org/posts/bounded-rands.html
        // It's the fastest known algorithm for unbiased random numbers in a
        // range.  It's a little more complex than other techniques.
        uint32_t x = rng_();
        uint64_t m = uint64_t(x) * uint64_t(max);
        uint32_t l = uint32_t(m);
        if (l < max) {
            uint32_t t = -max;
            if (t >= max) {
                t -= max;
                if (t >= max) {
                    t %= max;
                }
            }
            while (l < t) {
                x = rng_();
                m = uint64_t(x) * uint64_t(max);
                l = uint32_t(m);
            }
        }
        return m >> 32;
    } else {
        std::uniform_int_distribution<uint32_t> udist(0, max - 1);
        return udist(rng_);
    }
}

uint32_t RandUInt32::get() {
    if constexpr (is_fullwidth_prng<rng_type>) {
        // If so, we don't need to do anything to the result.
        return rng_();
    } else {
        std::uniform_int_distribution<uint32_t> udist(
            0, std::numeric_limits<uint32_t>::max());
        return udist(rng_);
    }
}

Queries

  • Duck speaking

    What's jsf32?

  • LHS Cow speaking

    It's a C++-style PRNG that is fast and compact. The original algorithm was invented by Bob Jenkins and implemented in C, but we use this C++ implementation.

  • RHS Cow speaking

    Prof. Melissa discusses it in depth on her blog here and here.

  • Duck speaking

    Are there other generators I can use?

  • LHS Cow speaking

    Yes, a lot. There are a number built into the C++ standard library, but they tend to be older generators that have various flaws.

  • RHS Cow speaking

    The std::mt19937 generator (a.k.a. The Mersenne Twister) is pretty good, but uses up a lot of space -- at least 19937 bits or about 2.5 kiB, and some implementations use twice that to gain added speed.

  • LHS Cow speaking

    Technically, the Mersenne Twister fails some PRNG test suites (failing the Matrix Rank and Linear Complexity tests).

  • Cat speaking

    What about Prof. Melissa's PRNG?

  • LHS Cow speaking

    You can download a C++ implementation of PCG from GitHub.

  • Goat speaking

    Why don't you use it here?

  • LHS Cow speaking

    Prof. Melissa, as a matter of principle, prefers not to require anyone to use her PRNG.

(When logged in, completion status appears here.)