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 |
- 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()andTool::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:
- Keep using
Tool::randMWC()it’s fast and excellent quality - When v0.15.0 releases, consider switching to
Tool::randPCG32()for the best overall performance - 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:
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!



