Skip to content

Performance Random Performance

github-actions[bot] edited this page Aug 28, 2026 · 23 revisions

Random Number Generator Performance Benchmarks

State repair for a generator restored from JSON or protobuf happens in its constructor or the shared after-deserialization callback. This includes each generator's state, the shared bit/byte reservoirs and GUID scratch buffer. Repair is not repeated in a draw method, so malformed/default serialized state is repaired before the first draw without adding a guard to every later draw. PhotonSpin's one-time warmup priming also happens there, leaving only its block-boundary check in the draw path.

How the Speed column is measured

The ops/s columns are each generator measured on its own, one after another. That answers "how fast is this generator here" and it is the wrong instrument for "which generator is faster": the roster takes minutes to walk, and anything that changed on the machine in between lands on whichever generator was being measured at the time.

The Speed column is measured differently. Every generator is compared against IllusionFlow (the speed baseline, measured in the same ABBABAAB batch) so each of the four readings that make up a ratio sits next to the reading it is divided by, and both generators occupy the same mean position in the batch. A drift that is linear across the batch cancels rather than being attributed to one side. The four raw readings per side are kept, and their spread is what decides whether the ratio is worth publishing: if the machine moved more than 3% between adjacent cycles, that generator falls back to the un-paired number and the run says so.

So a Speed bucket is a claim about the generators; an ops/s figure is a claim about this machine on that day.

Two batteries, because they ask different questions

PractRand streams until something fails and reports the depth: a generator is "clean through 8GB" or it is not. TestU01 SmallCrush runs fifteen fixed statistics and reports a p-value for each. A generator can be clean at 8GB of PractRand and still land a decisive p-value here, so neither stands in for the other.

Both run against the same byte stream, from the same host, so a difference between them is a difference between the batteries rather than between two ways of producing bytes.

Reading a SmallCrush result takes one piece of context: with fifteen statistics, a perfectly good generator lands one p-value outside [0.001, 0.9990] roughly one run in seven. The threshold that separates noise from signal is not close: the recorded weak control reports eps (below 1e-300), while noise sits around 1e-4. Anything below 1e-10 is treated as a failure; anything above it is reported and ignored. IllusionFlow produced a single 7.2e-4 at the manifest seed and was clean on two other seeds, which is what that rule is for.

Whole-inventory SmallCrush, 2026-08-26, seed 00010203-0405-0607-0809-0a0b0c0d0e0f, 908 MB and 6.3 s of CPU per generator:

  • Every generator rated Good or better passed.
  • Four recorded-weak generators failed decisively, most of their statistics at eps: LinearCongruentialGenerator, WDoomRandom, WaveSplatRandom, XorShiftRandom.
  • Three recorded-weak generators passed: DotNetRandom, SquirrelRandom, SystemRandom. SmallCrush is the shallower instrument, so that is inconclusive rather than a contradiction of their rating.

For statistical batteries, the repository's Generator~/WallstopStudios.UnityHelpers.RandomQuality host emits a reproducible little-endian byte stream from an explicit generator, GUID seed and byte count. Long PractRand/TestU01 runs belong in scheduled reporting with pinned tools and expected weak-generator failures; they are not suitable as nondeterministic pull-request gates.

The NextUlong, NextLong and NextDouble columns below predate the one-advance-per-64-bit-draw change to BlastCircuitRandom, RomuDuo, SplitMix64, WyRandom and Xoshiro256StarStar, which is measured at 2.49x on Unity 6000.4.6f1 (Mono). They refresh the next time RandomPerformanceTests.Benchmark runs.

Summary (fastest first)

Random NextUint (ops/s) Speed Quality Notes
LinearCongruentialGenerator 1,010,900,000 Fastest Poor Numerical Recipes 'quick and dirty' LCG (a=1664525, c=1013904223, m=2^32) returning the raw state, so bit k has period only 2^(k+1) -- measured linear complexity of bit k is 2^k+1, and bit 0 simply alternates. Cosmetic use only.
WaveSplatRandom 829,000,000 Very Fast Experimental Single-word chaotic generator; author notes period 2^64 but provides no formal test results—treat as experimental.
XorShiftRandom 742,700,000 Fast Fair Classic 32-bit xorshift; known to fail portions of TestU01 and PractRand, acceptable for lightweight effects only. Marsaglia 2003
RomuDuo 702,200,000 Fast Good Published romuDuo: the ROMU multiplier with the ROTL(y,36) + ROTL(y,15) - x update. NextUint returns the low 32 bits of the 64-bit word. Overton 2020
XoroShiroRandom 704,000,000 Fast Good xoroshiro128+, returning the high 32 bits -- the half its authors recommend. The discarded low half is linear (bit 0 has linear complexity exactly 128); no output bit of the returned half is. A 64-bit draw costs two state advances, because a + scrambler has no strong 64-bit word to return. Blackman & Vigna 2018
SplitMix64 701,500,000 Fast Very Good Well-known SplitMix64 mixer; passes TestU01 BigCrush and PractRand up to large data sizes in literature. Vigna 2014
FlurryBurstRandom 605,200,000 Fast Excellent Six-word ARX-style generator tuned for all-around use. Verified here: PractRand 0.95 clean through 8GB, the depth at which SystemRandom fails. The author reports TestU01 BigCrush passes; that run cannot be checked -- the upstream repository is offline.
BlastCircuitRandom 594,900,000 Fast Good Chaotic ARX mixer rather than a proven statistically optimal generator. Verified here: PractRand 0.95 clean through 8GB, the depth at which SystemRandom fails.
PcgRandom 590,800,000 Fast Excellent PCG XSH RR 64/32 variant; passes TestU01 BigCrush and PractRand in published results. O'Neill 2014
Sfc64Random 527,500,000 Moderate Very Good sfc64 (Small Fast Chaotic): three 64-bit words plus a draw counter, seeded by the canonical twelve-draw warm-up. NextUint returns the upper half of the output word, where every mixed bit arrives. O'Neill 2018 (Doty-Humphrey's SFC)
Xoshiro256StarStar 503,500,000 Moderate Excellent xoshiro256** 1.0; the ** scrambler leaves no weak bit, and the native 64-bit word means NextUlong costs one state advance instead of the two every other 64-bit generator here needs. Blackman & Vigna 2018
StormDropRandom 501,200,000 Moderate Excellent Large-state ARX generator over a 1024-word (4 KB) ring buffer with two 32-bit control words. Verified here: PractRand 0.95 clean through 8GB, the depth at which SystemRandom fails. The author's own results cannot be checked -- the upstream repository is offline.
IllusionFlow 500,500,000 Moderate Excellent Five-word rotate/xor/add generator driven by a 32-bit Weyl counter. Verified here: PractRand 0.95 clean through 8GB, the depth at which SystemRandom fails. The author reports 64GB; that run cannot be checked -- the upstream repository is offline.
Xoshiro128StarStar 482,000,000 Moderate Excellent xoshiro128** 1.1; the ** scrambler leaves no weak bit, so NextBool and low-bit masks are as strong as the full word. Native 32-bit output, so NextUint discards nothing. Blackman & Vigna 2018
SquirrelRandom 351,100,000 Slow Fair Hash-based generator built on Squirrel3. Measured: fails PractRand 0.95 FPF-14+6/16 at 1GB, reproducibly across four seeds. Good equidistribution for the table lookups it was designed for; not a general-purpose stream. Squirrel Eiserloh
WyRandom 299,500,000 Slow Very Good Wyhash-based generator; published testing shows it clears BigCrush/PractRand with wide seed coverage. Wang Yi 2019
PhotonSpinRandom 246,000,000 Slow Excellent SHISHUA-inspired generator. Verified here: PractRand 0.95 clean through 8GB, the depth at which SystemRandom fails. The author reports 128GB; that run cannot be checked -- the upstream repository is offline.
WDoomRandom 195,400,000 Very Slow Poor Index-into-array generator over a fixed 1024-entry table of 32-bit values. One entry serves one NextUint, so the period is exactly 1024 draws. Deterministic by design, not statistically random.
UnityRandom 113,800,000 Very Slow Fair Mirrors UnityEngine.Random, documented by Unity as Xorshift 128; suitable for legacy compatibility but not high-stakes simulation. UnityEngine.Random
SystemRandom 59,200,000 Very Slow Poor Reimplements the classic .NET System.Random algorithm (Knuth subtractive lagged-Fibonacci, mod 2^31-1) so its sequence is serializable and stable across runtimes. Fails modern statistical batteries. System.Random considered harmful
DotNetRandom 47,000,000 Very Slow Poor Wraps System.Random, which on Mono and for seeded .NET Core is Knuth's subtractive lagged-Fibonacci generator (mod 2^31-1), not an LCG. The sequence is runtime-dependent, so do not rely on it for cross-platform determinism. System.Random considered harmful

Detailed Metrics

Random NextBool Next NextUint NextFloat NextDouble NextUint (Range) NextInt (Range)
LinearCongruentialGenerator 717,900,000 507,500,000 1,010,900,000 180,700,000 275,300,000 380,700,000 302,600,000
WaveSplatRandom 712,700,000 413,800,000 829,000,000 175,400,000 246,700,000 350,100,000 304,100,000
XorShiftRandom 711,000,000 474,500,000 742,700,000 173,200,000 235,000,000 326,500,000 304,100,000
RomuDuo 700,500,000 454,500,000 702,200,000 169,600,000 414,100,000 324,500,000 303,600,000
XoroShiroRandom 715,800,000 455,400,000 704,000,000 169,500,000 222,700,000 326,700,000 285,900,000
SplitMix64 727,600,000 455,500,000 701,500,000 168,800,000 413,000,000 324,800,000 301,800,000
FlurryBurstRandom 731,100,000 412,000,000 605,200,000 162,700,000 197,800,000 282,800,000 283,200,000
BlastCircuitRandom 628,900,000 454,800,000 594,900,000 169,200,000 396,100,000 302,700,000 284,900,000
PcgRandom 719,100,000 414,300,000 590,800,000 163,400,000 198,500,000 285,500,000 285,000,000
Sfc64Random 708,900,000 414,600,000 527,500,000 162,500,000 380,300,000 302,300,000 285,800,000
Xoshiro256StarStar 615,800,000 380,200,000 503,500,000 156,900,000 343,700,000 283,200,000 260,400,000
StormDropRandom 705,800,000 353,200,000 501,200,000 154,400,000 178,000,000 268,900,000 247,700,000
IllusionFlow 706,200,000 412,200,000 500,500,000 161,600,000 198,500,000 294,500,000 268,400,000
Xoshiro128StarStar 672,800,000 380,200,000 482,000,000 160,000,000 189,900,000 285,200,000 268,300,000
SquirrelRandom 720,200,000 323,800,000 351,100,000 144,100,000 161,000,000 247,400,000 239,700,000
WyRandom 672,100,000 237,900,000 299,500,000 126,500,000 230,100,000 200,000,000 185,200,000
PhotonSpinRandom 663,300,000 207,500,000 246,000,000 113,000,000 101,100,000 165,400,000 158,000,000
WDoomRandom 694,800,000 145,100,000 195,400,000 106,400,000 74,800,000 163,700,000 156,700,000
UnityRandom 623,800,000 100,000,000 113,800,000 74,100,000 48,400,000 95,100,000 94,200,000
SystemRandom 130,600,000 143,800,000 59,200,000 113,200,000 116,900,000 53,400,000 52,000,000
DotNetRandom 494,600,000 44,900,000 47,000,000 36,500,000 22,000,000 42,500,000 41,800,000

Generators added since the last benchmark run

The tables above are rewritten only by a benchmark run, so a generator added since the last one is absent until the .github/workflows/unity-benchmarks.yml workflow next runs. Absence here says nothing about quality: statistical standing is measured separately, by the bit-plane linearity gate on every pull request and by the scheduled PractRand battery. See Random Generators for the current ratings.

The battery runs both stream widths. NextUlong is no longer NextUint rearranged: five generators answer a 64-bit draw from one raw word, so half of it reaches a caller only through NextDouble, NextLong and NextUlong(max) and appears in no 32-bit draw. Even the generators that do build NextUlong from two NextUint draws pack them high-word-first and write little-endian, so their 64-bit stream is the 32-bit one with each adjacent word pair swapped. SystemRandom is the proof that this is not a redundant measurement: it fails the 32-bit battery at exactly 8GB and is clean through 8GB at 64-bit. Every "clean through 8GB" above is the 32-bit figure; the 64-bit outcomes are recorded per generator in scripts/random-quality/expected-outcomes.json.

Refreshing these numbers

Run RandomPerformanceTests.Benchmark from Unity's Test Runner, or let the weekly Unity Benchmarks workflow do it. Both rewrite the tables in place.

Clone this wiki locally