.. | ||
README.md | ||
rng.go | ||
UNLICENSE |
Alternative Go PRNGs
Package rng
provides several more efficient, higher quality PRNGs
sources for use with math/rand.Rand
. Each PRNG implements the
math/rand.Source64
interface.
What makes these PRNGs more efficient? They have tiny states — 32 bytes for the largest — compared to gc's default source, which has a ~5kB state. Two of the generators run faster, too. See the benchmarks below.
What generators are included?
- SplitMix64
- 32-bit and 64-bit permuted congruential generator (PCG)
- Custom 64-bit PCG using xorshift-multiply permutation (Pcg64x)
- xoshiro256**
- A "minimal standard" 128-bit linear congruential generator (LCG)
- A 64-bit Middle Square Weyl Sequence
- RomuDuo and RomuDuoJr of the Romu family
SplitMix64 is the fastest generator. Pcg64x is the fastest robust generator.
Example
package main
import (
"fmt"
"math/rand"
"nullprogram.com/x/rng"
)
func main() {
s := new(rng.Lcg128)
s.Seed(1)
r := rand.New(s)
fmt.Printf("%v\n", r.NormFloat64())
fmt.Printf("%v\n", r.NormFloat64())
fmt.Printf("%v\n", r.NormFloat64())
// Output:
// -0.5402515557248266
// 0.00984877400071782
// -0.40951475107890106
}
Benchmark
The gc implementation of Go doesn't go a great job optimizing these
routines compared to either GCC or Clang, so SplitMix64 performs the
best of the algorithms in this package. The "baseline" is the default
source from math/rand
, and the "interface" benchmarks call through the
math/rand.Source64
interface.
$ go test -bench=.
goos: linux
goarch: amd64
pkg: nullprogram.com/x/rng
BenchmarkLcg128-8 429435103 2.72 ns/op
BenchmarkLcg128Interface-8 305827086 3.94 ns/op
BenchmarkSplitMix64-8 869916241 1.37 ns/op
BenchmarkSplitMix64Interface-8 349949367 3.40 ns/op
BenchmarkXoshiro256ss-8 345413484 3.49 ns/op
BenchmarkXoshiro256ssInterface-8 219676675 5.58 ns/op
BenchmarkPcg32-8 338895014 3.54 ns/op
BenchmarkPcg32Interface-8 249454053 4.90 ns/op
BenchmarkPcg64-8 223279998 5.40 ns/op
BenchmarkPcg64Interface-8 169870239 7.06 ns/op
BenchmarkPcg64x-8 590234182 2.08 ns/op
BenchmarkPcg64xInterface-8 285790071 4.18 ns/op
BenchmarkMsws64-8 347618887 3.43 ns/op
BenchmarkMsws64Interface-8 245472339 4.89 ns/op
BenchmarkRomuDuo-8 581715128 2.03 ns/op
BenchmarkRomuDuoInterface-8 331822014 3.67 ns/op
BenchmarkRomuDuoJr-8 643661610 1.87 ns/op
BenchmarkRomuDuoInterfaceJr-8 332948454 3.62 ns/op
BenchmarkBaseline-8 298460190 4.05 ns/op
The big takeaway here: Interface calls are relatively expensive! If possible, use SplitMix64, and do not call it through an interface since that cuts its performance in half. If you must call through an interface, the built-in PRNG is the fastest, though has the worst quality and a large state.
Statistical Quality
generator | dieharder | BigCrush | PractRand |
---|---|---|---|
math/rand | PASS | 1 fail | 256MB |
Lcg128 | PASS | 1 fail | 128GB |
SplitMix64 | PASS | 1 fail | > 8TB |
Xoroshiro256ss | PASS | PASS | > 8TB |
Pcg32 | PASS | 1 fail | > 8TB |
Pcg64 | PASS | PASS | > 8TB |
Pcg64x | PASS | PASS | > 8TB |
Msws64 | PASS | PASS | > 8TB |
RomuDuo | PASS | PASS | > 8TB |
RomuDuoJr | PASS | 3 fail | > 8TB |
Tests were run with a zero seed, dieharder 3.31.1, TestU01 1.2.3, and PractRand 0.95. PractRand was stopped after 8TB of input.