Splitmix32: Thirteen Lines of Beautiful Randomness
On this page
I was building interactive widgets for this blog that needed reproducible randomness. A simulation showing greedy vs. random algorithms, where both strategies operate on the same sequence of inputs. In Python, you write random.seed(42) and you get the same sequence every time. Deterministic. Replayable. Checkpointable. JavaScript gives you Math.random(), which provides none of that: no seed parameter, no way to replay a sequence, no way to share one across environments.
I went looking for a drop-in replacement and found splitmix32. It stopped me in my tracks. Not because it was fast (it is), or because it was small (it is), but because every single line of it is doing something beautiful. Thirteen lines of TypeScript, and each one earns its place.
The full 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;
};
}
That is the entire thing. Feed in a seed, get back a closure that produces deterministic floats in . Each call advances the internal state and returns the next value. Same seed, same sequence, every time. The function is a direct port of the 32-bit variant from the SplitMix family introduced by Steele, Lea, and Flood at OOPSLA 2014, with mixing constants from Chris Wellons’ hash-prospector project.
The function has two moving parts: a Weyl sequence that drives the state forward, and a mixing function that destroys patterns. The best way to build intuition is to watch it execute.
Watch it run
Enter a seed below and step through each line. The variable inspector on the right tracks state and t in both decimal and hex. For XOR-shift steps, a bit-level breakdown shows exactly which bits flipped. Use arrow keys to step forward and back.
If you stepped through the code, you noticed two unusual JavaScript operators: >>> and Math.imul. Both are essential to how splitmix32 works at the bit level.
Two operators you should know
The unsigned right shift: >>>
JavaScript has two right-shift operators. The familiar >> is an arithmetic shift: it preserves the sign bit, filling vacated positions with copies of it. A negative number stays negative after >>. The triple-arrow >>> is a logical shift: it always fills with zeros, regardless of the sign bit. The result is always non-negative.
Splitmix32 uses >>> everywhere because it treats all 32 bits as data. There is no sign bit; there are just 32 bits of state being shuffled around. The >>> 0 at the end of the return line is particularly important: it reinterprets the final value as an unsigned integer before dividing by , ensuring the output lands in rather than .
JavaScript has two right-shift operators. >> preserves the sign bit (arithmetic shift). >>> fills with zeros (logical shift). The default value is negative; notice how the results diverge. Switch to a positive value to see them agree.
>> copies the sign bit (1) into vacated positions, keeping the result negative. >>> fills with 0, producing a large positive number. Splitmix32 uses >>> because it treats all 32 bits as data, not sign + magnitude.Why Math.imul instead of *
JavaScript numbers are IEEE 754 doubles: 64 bits of floating-point with 53 bits of integer precision. For small numbers, * works fine. For two large 32-bit integers, the product can exceed , and JavaScript silently rounds the result. The low-order bits vanish.
Math.imul computes the true low 32 bits of the product, as if both operands were 32-bit integers multiplied with infinite precision and then truncated. No float promotion, no silent rounding. The mixer constants in splitmix32 are large enough that every single multiply would lose bits without it.
JavaScript's * operator uses floating-point math with 53 bits of integer precision. Multiplying two large 32-bit values can produce a result beyond 253, silently corrupting the low bits. Math.imul computes the true 32-bit product without float promotion.
First multiply in splitmix32(42). The product exceeds 253, so plain JS loses precision.
| 0: 0x976fc700 = -1754282240| 0 truncates, producing 0x976fc700 instead of the correct 0x976fc774. A difference of 116 in the low bits.The Weyl sequence: 0x9e3779b9
The state advances by a fixed increment on 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.
A Weyl sequence alone is deterministic and full-period, but its output has obvious structure: consecutive values differ by a constant. The mixer exists to destroy that structure.
The mixing function
Three rounds of xorshift-multiply transform the predictable Weyl output into something that passes statistical randomness tests:
// 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 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 core reason the output looks random.
The constants 0x21f0aaad and 0x735a2d97 were discovered computationally. Wellons’ hash-prospector 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 16, 15, and 15 were co-optimized alongside the multipliers.
Why it is 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, one group at a time. Multiplication by any odd number is also invertible, since odd numbers are coprime to (they have a multiplicative inverse in the ring ). Both 0x21f0aaad and 0x735a2d97 are odd. The consequence: every input maps to a unique output. No collisions, no wasted states.
When to use it, when not to
splitmix32 | Math.random() | crypto.getRandomValues() | |
|---|---|---|---|
| Seedable | Yes | No | No |
| Reproducible | Yes, same seed = same sequence | No | No |
| Period | (~4.3 billion) | Implementation-dependent | N/A |
| State size | 32 bits | Implementation-dependent | 128+ bits |
| Speed | ~1 ns/call | ~1 ns/call | ~50-200 ns/call |
| Cryptographic | No | No | Yes |
| Dependencies | Zero | Built-in | Built-in |
Use splitmix32 when you need a deterministic sequence and security is not a concern: simulations that must be reproducible across runs, procedural generation where the same seed should always produce the same world, property-based tests that need a replayable source of randomness, or seeding other, larger PRNGs. The 64-bit SplitMix is the default seeder for Java’s SplittableRandom and for the xoshiro/xoroshiro generators.
Do not use it for anything security-sensitive. With only 32 bits of state, an attacker who observes a single output can brute-force the seed in under a second on modern hardware. Use crypto.getRandomValues() instead. If your application needs trillions of draws, the period is also too short; consider a 64-bit generator like xoshiro256** instead.