C++20 Header-only library: A "fast jump" algorithm for LCG
Library: src/include/lcg_predict.hpp.
Test: src/test/lcg_predict_test.cpp.
To use: Include lcg_predict.hpp.
To test: Compile lcg_predict_test.cpp. If it compiles, it passes the test. There is no need to run the generated executable.
Given T as an unsigned integer type, the library exposes:
- Class
LCGAffineTransform<T>: combines threea,candmof typeT, describing an affine transformation$x \mapsto (ax + c) \bmod m$ . - Class
LCGEngine<T>: combines anLCGAffineTransform<T>and an internalstateof typeT. - Some instances of
LCGEngine<T>, corresponding to some widely used LCGs.
Overview:
namespace ls_hower::lcg_predict {
template <std::unsigned_integral UIntType>
class LCGAffineTransform {
public:
using result_type = UIntType;
explicit constexpr LCGAffineTransform(UIntType a, UIntType c, UIntType m = 0) noexcept;
template <typename StdEngine>
static constexpr auto from_std() noexcept -> LCGAffineTransform
requires detail::std_lcg_of<std::remove_cvref_t<StdEngine>, UIntType>
constexpr auto set_a(UIntType a) noexcept -> void;
constexpr auto set_c(UIntType c) noexcept -> void;
constexpr auto set_m(UIntType m) noexcept -> void;
constexpr auto a() const noexcept -> UIntType;
constexpr auto c() const noexcept -> UIntType;
constexpr auto m() const noexcept -> UIntType;
constexpr auto operator()(result_type x) const noexcept -> result_type;
// Hidden friend
friend constexpr auto operator+(LCGAffineTransform lhs, const LCGAffineTransform& rhs) noexcept -> LCGAffineTransform;
constexpr auto operator+=(const LCGAffineTransform& rhs) noexcept -> LCGAffineTransform&;
// Hidden friend
friend constexpr auto operator-(LCGAffineTransform lhs, const LCGAffineTransform& rhs) noexcept -> LCGAffineTransform;
constexpr auto operator-=(const LCGAffineTransform& rhs) noexcept -> LCGAffineTransform&;
// Hidden friend
// Returns h such that h(x) = lhs(rhs(x)).
friend constexpr auto compose(LCGAffineTransform lhs, const LCGAffineTransform& rhs) noexcept -> LCGAffineTransform;
constexpr auto compose_assign(const LCGAffineTransform& rhs) noexcept -> LCGAffineTransform&;
constexpr auto identity() const noexcept -> LCGAffineTransform;
// Returns h such that h(x) = f(f(f(...f(x)...))) where f is `*this`, and there are `n` f's.
// Time complexity: O(log(n)).
constexpr auto powered(unsigned long long n) const noexcept -> LCGAffineTransform;
constexpr auto min() const noexcept -> result_type;
constexpr auto max() const noexcept -> result_type;
// Hidden friend
// `= default;`
friend constexpr auto operator==(const LCGAffineTransform& lhs, const LCGAffineTransform& rhs) noexcept -> bool;
};
template <std::unsigned_integral UIntType>
class LCGEngine {
public:
using result_type = UIntType;
using affine_type = LCGAffineTransform<UIntType>;
static constexpr UIntType default_seed { 1U };
explicit constexpr LCGEngine(affine_type affine, result_type state = default_seed) noexcept;
explicit constexpr LCGEngine(UIntType a, UIntType c, UIntType m = 0, result_type state = default_seed) noexcept;
template <typename StdEngine>
static auto from_std(StdEngine engine) noexcept(false) -> LCGEngine
requires detail::std_lcg_of<std::remove_cvref_t<StdEngine>, UIntType>
constexpr auto operator()() noexcept -> result_type;
// Time complexity: O(log(n)).
constexpr auto value_after_n_steps(unsigned long long steps) const noexcept -> result_type;
// Time complexity: O(log(n)).
constexpr auto discard(unsigned long long n) noexcept -> void;
constexpr auto a() const noexcept -> UIntType;
constexpr auto c() const noexcept -> UIntType;
constexpr auto m() const noexcept -> UIntType;
constexpr auto affine() const noexcept -> affine_type;
constexpr auto state() const noexcept -> result_type;
constexpr auto set_a(UIntType new_a) noexcept -> void;
constexpr auto set_c(UIntType new_c) noexcept -> void;
constexpr auto set_m(UIntType new_m) noexcept -> void;
constexpr auto set_affine(const affine_type& new_affine) noexcept -> void;
constexpr auto set_state(result_type new_seed) noexcept -> void;
// Hidden friend
// `= default;`
friend constexpr auto operator==(const LCGEngine& lhs, const LCGEngine& rhs) noexcept -> bool;
};
// LCG suggested in K&R C and C standards.
constexpr inline LCGEngine<std::uint_fast32_t> krc_rand_engine { 1103515245, 12345, 2147483648 };
// C++ `std::minstd_rand`.
constexpr inline LCGEngine<std::uint_fast32_t> minstd_rand_engine { 48271, 0, 2147483647 };
// C++ `std::minstd_rand0`.
constexpr inline LCGEngine<std::uint_fast32_t> minstd_rand0_engine { 16807, 0, 2147483647 };
// MSVC `std::rand`.
constexpr inline LCGEngine<std::uint_fast32_t> msvc_rand_engine { 214013, 2531011, 2147483648 };
// POSIX `*rand48`.
constexpr inline LCGEngine<std::uint_fast64_t> posix_rand48_engine { 25214903917, 11, 281474976710656 };
// Musl `rand`.
constexpr inline LCGEngine<std::uint_fast64_t> musl_rand_engine { 6364136223846793005, 1, 0 };
} // namespace ls_hower::lcg_predictThe test verifies that the pesudo-random number sequences gotten from:
- simulating (calling
operator()) - predicting (calling
value_after_n_steps(i)) - other source (OEIS)
are the same, for initial 10 terms.
It also checks that sequences generated by simulating and predicting are the same, for initial 1000 terms.
These are done for all pre-defined engines.
It also checks:
minstd_rand0_engine.value_after_n_steps(10000) == 1043618065minstd_rand_engine.value_after_n_steps(10000) == 399268537
corresponding to the required behavior of std::minstd_rand0 and std::minstd_rand in C++ standard.
The whole test is evaluated at compile-time, checked by static_assert. So it passes the test if the code compiles.
So here is a classic C implementation of rand() function.
unsigned long int next = 1;
/* rand: return pseudo-random integer on 0..32767 */
int rand(void)
{
next = next * 1103515245 + 12345;
return (unsigned int)(next/65536) % 32768;
}
/* srand: set seed for rand() */
void srand(unsigned int seed)
{
next = seed;
}This implementation is taken from The C Programming Language (TCPL), 2nd edition, by Kernighan and Ritchie, as an example of a portable implementation of rand() and srand(). Variants of this code also appear in the C standard.
We can observe that on each call to rand(), the global variable next is updated by multiplying it by 1103515245 and adding 12345. The function then returns bits 16 through 31 of next; this output operation does not affect the value of next itself. The function srand() simply assigns a new initial value to next, which can be regarded as resetting the seed.
Let us define a sequence
Here, srand(). This sequence exactly describes the evolution of the variable next, restricted to its lower 31 bits. Any higher bits are irrelevant.
As mentioned earlier, rand() does not return next directly. Instead, the value returned by the rand() is
This mapping is straightforward to compute, so in the following discussion we focus solely on the sequence
We now generalize this construction:
where
In the previous example, we have
By expanding the recurrence, we observe that
In general,
This formula can be proved by induction.
Using exponentiation by squaring,
If the multiplicative inverse
The inverse
The inverse-based method does not apply when
The following Python code implements this idea:
def fast_sum_and_pow(x: int, y: int, m: int) -> int:
"""
Returns (init, last) where:
init = (1 + x + x**2 + ... + x**(y-1)) % m
last = (x**y) % m
"""
if y == 0:
return 0 % m, 1 % m
elif y % 2 == 1:
init, last = fast_sum_and_pow(x, y - 1, m)
return (1 + init * x) % m, (last * x) % m
else:
init, last = fast_sum_and_pow(x, y // 2, m)
return (init + init * last) % m, (last * last) % m
def fast_sum(x: int, y: int, m: int) -> int:
"""Returns (1 + x + x**2 + ... + x**(y-1)) % m."""
return fast_sum_and_pow(x, y, m)[0]Calling fast_sum(a, n, m) produces the desired value of
There is an even simpler approach: compute
Consider two affine transformations:
Let
Thus,
An affine transformation can therefore be represented by a pair of integers
It is straightforward to see that
where
This recurrence immediately yields an
All three methods discussed above can be applied to generalized LCGs. For example, those where
This library, however, uses only the fast affine transformation composition method to implement fast jumping and to predict the