Testing Random Number Generators in CRM64Pro

As I continue developing CRM64Pro, I’ve been thinking about the random number generators (RNGs) we provide. Currently, the library includes two alternatives: a Multiply-with-Carry (MWC) implementation and WELL512. But are these the best choices for 2D game developers in 2026?

I decided to do some proper benchmarking and simple quality testing. I tested the existing options against a couple of modern alternatives that keep popping up in discussions: PCG32 and Xoshiro256++. The goal? Figure out what’s actually good, what’s not, and potentially add a better default option to CRM64Pro.

The contenders

Before diving into results, here’s a quick rundown of what each generator is:

  • Standard rand() – The classic C library function. Typically implemented as a Linear Congruential Generator (LCG): the next value is computed by multiplying the previous one, adding a constant, and taking the remainder. Very simple and widely available, but based on an old design.
  • Tool::randMWC()CRM64Pro’s Multiply-with-Carry implementation. It multiplies the current value by a constant while keeping an additional carry value, similar to manual multiplication. Simple, fast, and with better statistical behavior than basic LCGs.
  • Tool::randWELL()CRM64Pro’s implementation of WELL512. It uses 512 bits of internal state and applies a sequence of XOR and shift operations. More complex than MWC, and designed to provide stronger statistical properties.
  • LCG (Knuth) – A Linear Congruential Generator using constants recommended by Donald Knuth. Still simple in structure, but generally better behaved than the standard rand().
  • PCG32 – A modern generator (2014) that combines a simple LCG with an output permutation step. This design improves statistical quality while keeping the generator small and efficient.
  • Xoshiro256++ – A modern generator (2018) from the authors of the Mersenne Twister successor. It uses XOR, shift, and rotate operations with a relatively large internal state, providing high statistical quality with good performance on modern CPUs.

The testing setup

I ran tests on four different CPUs to get a realistic picture:

  • AMD Ryzen 7 9800X3D (Windows 11).
  • AMD Ryzen 9 5900X (Windows 11).
  • Intel Core i7-4960HQ (Windows 10).
  • AMD Ryzen 5 2400G (Windows 10).

The quality tests implemented are simple: they are designed to highlight obvious distribution or bit-level issues, not to replace comprehensive statistical test suites (NIST, TestU01).

  • Monte Carlo Pi (2D): Generate random points and see how many fall inside a quarter circle. Should converge to π/4 in expectation over many random samples. A good approximation does not guarantee high-quality randomness, but large or systematic deviations may indicate distribution problems.
  • Monte Carlo volume (3D): Same idea but in 3D space. Should converge to π/6 in expectation over many random samples.
  • Bit frequency: Do all bits appear equally often? Should be 50% ones, 50% zeros.
  • Modulo bias: When you use rand()%3, does each value (0, 1, 2) appear equally often?. Modulo bias tests can reveal weaknesses in low-order bits, especially in simple generators

Testing methodology: All benchmarks run in single-threaded mode with compiler optimizations enabled. Monte Carlo, Bit frequency and Modulo bias tests used 10 million iterations.

Performance results

Here’s the raw speed across all four CPUs, shown as operations per second (millions):

Generator Ryzen 9800X3D Ryzen 5900X i7-4960HQ Ryzen 2400G Avg Speedup
Standard rand() 270.4 M 158.7 M 43.0 M 49.0 M baseline
Tool::randMWC() 1080.2 M 489.8 M 397.5 M 373.3 M 5.9x faster
Tool::randWELL() 387.8 M 347.4 M 174.7 M 170.4 M 2.0x faster
LCG (Knuth) 1355.3 M 1182.1 M 403.2 M 351.6 M 6.7x faster
PCG32 1354.4 M 1221.9 M 405.4 M 353.7 M 6.7x faster
Xoshiro256++ 361.9 M 289.4 M 232.2 M 219.1 M 2.1x faster

Ryzen 9800X3D performanceRyzen 5900X performanceCore i7-4960HQ performanceRyzen 2400G performance

  • Standard rand() is surprisingly slow. it’s the slowest option across every CPU tested.
  • PCG32 and LCG are neck-and-neck for speed, both running about 6-7x faster than standard rand(). On modern AMD chips (9800X3D and 5900X), they’re absolutely flying at over 1 billion ops/sec.
  • MWC is fast but varies by CPU, it’s the second-fastest on the newer Ryzen chips but doesn’t scale as well on older hardware.
  • WELL512 and Xoshiro256++ are mid-tier performance, both about 2x faster than standard rand(), which is respectable but not amazing.

Quality results (Lower is better)

Monte Carlo Pi (2D) – Average error

Generator Avg Error from π
Xoshiro256++ 0.000133
PCG32 0.000222
Tool::randMWC() 0.000340
Tool::randWELL() 0.000380
LCG (Knuth) 0.000449
Standard rand() 0.000538

Monte Carlo volume (3D) – Average error

Generator Avg Error
Standard rand() 0.000289
PCG32 0.000414
Tool::randMWC() 0.000670
Tool::randWELL() 0.000689
LCG (Knuth) 0.000918
Xoshiro256++ 0.001042

Note: Standard rand() looks good here, but this is misleading, read on to see why.

Bit frequency – Average deviation from 50%

Generator Avg Deviation
LCG (Knuth) 0.0024% ✓
Tool::randMWC() 0.0029%
PCG32 0.0028%
Xoshiro256++ 0.0043% ✓
Tool::randWELL() 0.0069% ✓
Standard rand() 53.1262%

This is devastating for standard rand(). A 53% deviation means one of the bits is almost always on or almost always off, that’s catastrophic. All other generators are essentially perfect (< 0.01% deviation).

Modulo bias (mod 3) – Average bias

Generator Avg Bias
Tool::randWELL() 0.0305%
Tool::randMWC() 0.0412% ✓
LCG (Knuth) 0.0521% ✓
Standard rand() 0.0534% ✓
Xoshiro256++ 0.0629% ✓
PCG32 0.0809% ✓

All generators show comparatively small bias for this specific mod 3 test. Modulo bias tests can reveal low-order bit issues, but their results depend on the modulus chosen and should be interpreted cautiously.

What these results actually mean

To put these results into context:

Standard rand() shows severe weaknesses, particularly in the bit frequency test, where the deviation is extremely large. Even when some Monte Carlo results appear numerically close to π, this should not be interpreted as good randomness. The fact that it’s also the slowest option makes this a no-brainer: don’t use it.

Xoshiro256++ provides excellent bit distribution and overall consistency. In our benchmarks, its performance is comparable to Tool::randWELL(), and on some older CPUs it can be noticeably faster.

PCG32 is the most well-rounded. It’s blazingly fast (tied for first), has excellent Monte Carlo results, perfect bit distribution, and only shows slightly higher modulo bias than the best (still negligible). This is the all-arounder.

Tool::randWELL() has the best modulo characteristics but it’s the slowest of the “good” generators and doesn’t show statistical advantages that justify the performance cost for game development.

Tool::randMWC() is fast and solid with good bit distribution, though it falls in the middle for Monte Carlo tests. It’s a good choice.

Results vary by CPU architecture: modern AMD Ryzen chips get massive performance from PCG32 and LCG, while older Intel chips show more even performance across generators. This suggests hardware-specific optimizations matter.

Conclusions

After looking at all this data, here’s what I think:

For CRM64Pro users:

  • Good news: CRM64Pro never defaulted to standard rand(). The library has always provided Tool::randMWC() and Tool::randWELL() as proper alternatives, and the data shows this was the right decission, both are significantly faster and have vastly better quality than the standard C library function.
  • Use Tool::randMWC() for general game development. It’s fast, has excellent bit distribution, and works well across different CPUs. This has been a solid default choice and continues to be.
  • Use Tool::randWELL() when you need the absolute best modulo behavior or if you’re doing something where statistical rigor matters.

What’s coming in CRM64Pro v0.15.0:

I’m adding PCG32 as the new recommended generator. The data is clear: it combines the speed of the fastest generators with excellent statistical quality across all tests. It’s also what modern engines like Godot use, and for good reason.

PCG32 will be available as Tool::randPCG32() in version 0.15.0. The implementation is simple (about 15 lines of code), has minimal state requirements (just 64 bits), and as we’ve seen, performs exceptionally well.

I’ll keep randMWC() and randWELL() available, they’re good generators with different trade-offs, and some users might prefer them for specific use cases. But for newcomers and the default recommendation, PCG32 is the way forward.

The bottom line

If you’re using CRM64Pro right now:

  1. Keep using Tool::randMWC()it’s fast and excellent quality
  2. When v0.15.0 releases, consider switching to Tool::randPCG32() for the best overall performance
  3. For special cases, Tool::randWELL() if you need perfect modulo distribution

If you’re implementing your own RNG in your 2D game? PCG32 is probably your best bet in 2026. It’s fast, it’s small, it’s statistically solid, and it’s battle-tested across the industry.

Want to test on your CPU?

Curious how these generators perform on your specific hardware? I’ve prepared a test program you can download and run:

Download RNG Benchmark tool

Run it and see how your CPU stacks up against the ones tested here. It’ll give you performance numbers and quality metrics for all six generators. If you get interesting results on different hardware, I’d love to hear about it!


Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.