Splitmix32: A Fast Seeded PRNG
A 32-bit seeded pseudorandom number generator derived from MurmurHash3's finalizer. Deterministic, fast, and good enough for simulations that need reproducibility — not cryptography.
Math.random() gives you no control over the sequence it produces — you cannot replay it, checkpoint it, or share it across environments. Simulations, procedural generation, and deterministic tests all need a seeded PRNG: feed in the same seed, get back the same sequence. Splitmix32 fills that role in 13 lines of TypeScript.
The implementation
export function splitmix32(seed: number):
() => number /* [0, 1) */ {
let state = seed | 0;
return () => {
state |= 0;
state = state + 0x9e3779b9 | 0;
let t = state ^ state >>> 16;
t = Math.imul(t, 0x21f0aaad);
t = t ^ t >>> 15;
t = Math.imul(t, 0x735a2d97);
return ((t = t ^ t >>> 15) >>> 0) / 4294967296;
};
}
Each call advances the internal state and returns a float in . JavaScript numbers are 64-bit floats by default, so the | 0 coercions force 32-bit integer semantics.
Without those coercions, additions like state + 0x9e3779b9 would silently overflow into floating-point territory. Math.imul serves the same purpose for multiplication: true 32-bit multiply, no float promotion.
Where it comes from
The SplitMix family was introduced by Steele, Lea, and Flood in Fast Splittable Pseudorandom Number Generators at OOPSLA 2014. That paper defines a 64-bit generator included in Java’s JDK8 (java.util.SplittableRandom). The 32-bit variant here adapts the same structure — a Weyl sequence counter feeding a mixing function — but swaps the 64-bit mixer for one derived from MurmurHash3’s fmix32 finalizer with improved constants.
The Weyl sequence: 0x9e3779b9
The state advances by a fixed increment each call:
a = a + 0x9e3779b9 | 0;
0x9e3779b9 is , where is the golden ratio. This forms a Weyl sequence: a progression modulo . The golden ratio has the slowest-converging continued fraction of any irrational number, which means stepping by this constant spreads values as uniformly as possible across the full 32-bit range. The period is exactly — every possible state is visited once before the sequence repeats.
The mixing function: 0x21f0aaad and 0x735a2d97
The Weyl sequence alone produces values with obvious structure — consecutive outputs differ by a constant. The mixer destroys that structure through three rounds of xorshift-multiply:
// Round 1: break up high-bit patterns
let t = a ^ a >>> 16;
t = Math.imul(t, 0x21f0aaad);
// Round 2: scramble remaining correlations
t = t ^ t >>> 15;
t = Math.imul(t, 0x735a2d97);
// Round 3: final diffusion
t = t ^ t >>> 15;
Each xorshift (x ^ x >>> n) copies high bits into lower positions, creating dependencies between bit groups. The subsequent multiply amplifies those dependencies across the full word — flip a single input bit, and roughly half the output bits flip with it. This property is called avalanche, and it is the reason the output passes statistical randomness tests.
In more precise terms, a perfect mixer would flip each output bit with a 50% chance for every flipped input bit. Real mixers deviate from this ideal; the deviation is measured as avalanche bias.
The specific constants 0x21f0aaad and 0x735a2d97 were discovered computationally. Chris Wellons’ hash-prospector project found them through hill climbing and genetic algorithms, optimizing for the lowest possible avalanche bias. These constants achieve a bias of , slightly beating the original MurmurHash3 fmix32 finalizer (0x85ebca6b / 0xc2b2ae35). The shift widths , , and were co-optimized alongside the multipliers.
Why it’s a bijection
The mixer is a composition of bijections on 32-bit integers. XOR-right-shift by bits is invertible because the top bits remain unchanged and can reconstruct the rest. Multiplication by any odd number is also invertible, since odd numbers are coprime to . Both 0x21f0aaad and 0x735a2d97 are odd. The consequence: every input maps to a unique output — no collisions, no wasted states.
When not to use it
Splitmix32 has only 32 bits of state, so an attacker who observes a single output can brute-force the seed in under a second. For anything security-sensitive, reach for crypto.getRandomValues(). For simulations, procedural content, and deterministic tests, splitmix32 is more than enough.