Search Apps Documentation Source Content File Folder Download Copy Actions Download

xorshift64star.gno

5.83 Kb ยท 172 lines
  1// Xorshift64* is a very fast psuedo-random number generation algorithm with strong
  2// statistical properties.
  3//
  4// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which
  5// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations
  6// because it has modest seeding requirements, and generates statistically strong randomness.
  7//
  8// This package provides an implementation of the Xorshift64* PRNG algorithm. This algorithm provides
  9// strong statistical performance with most seeds (just don't seed it with zero), and the performance
 10// of this implementation in Gno is more than four times faster than the default PCG implementation in
 11// `math/rand`.
 12//
 13//	Benchmark
 14//	---------
 15//	PCG:         1000000 Uint64 generated in 15.58s
 16//	Xorshift64*: 1000000 Uint64 generated in 3.77s
 17//	Ratio:       x4.11 times faster than PCG
 18//
 19// Use it directly:
 20//
 21//	prng = xorshift64star.New() // pass a uint64 to seed it or pass nothing to seed it with entropy
 22//
 23// Or use it as a drop-in replacement for the default PRNT in Rand:
 24//
 25//	source = xorshift64star.New()
 26//	prng := rand.New(source)
 27package xorshift64star
 28
 29import (
 30	"errors"
 31	"math"
 32
 33	"gno.land/p/demo/entropy"
 34	"gno.land/p/nt/ufmt"
 35)
 36
 37// Xorshift64Star is a PRNG that implements the Xorshift64* algorithm.
 38type Xorshift64Star struct {
 39	seed uint64
 40}
 41
 42// New() creates a new instance of the PRNG with a given seed, which
 43// should be a uint64. If no seed is provided, the PRNG will be seeded via the
 44// gno.land/p/demo/entropy package.
 45func New(seed ...uint64) *Xorshift64Star {
 46	xs := &Xorshift64Star{}
 47	xs.Seed(seed...)
 48	return xs
 49}
 50
 51// Seed() implements the rand.Source interface. It provides a way to set the seed for the PRNG.
 52func (xs *Xorshift64Star) Seed(seed ...uint64) {
 53	if len(seed) == 0 {
 54		e := entropy.New()
 55		xs.seed = e.Value64()
 56	} else {
 57		xs.seed = seed[0]
 58	}
 59}
 60
 61// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding.
 62// binary.bigEndian.Uint64, copied to avoid dependency
 63func beUint64(b []byte) uint64 {
 64	_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
 65	return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
 66		uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
 67}
 68
 69// bePutUint64() encodes a uint64 into a buffer of eight bytes.
 70// binary.bigEndian.PutUint64, copied to avoid dependency
 71func bePutUint64(b []byte, v uint64) {
 72	_ = b[7] // early bounds check to guarantee safety of writes below
 73	b[0] = byte(v >> 56)
 74	b[1] = byte(v >> 48)
 75	b[2] = byte(v >> 40)
 76	b[3] = byte(v >> 32)
 77	b[4] = byte(v >> 24)
 78	b[5] = byte(v >> 16)
 79	b[6] = byte(v >> 8)
 80	b[7] = byte(v)
 81}
 82
 83// A label to identify the marshalled data.
 84var marshalXorshift64StarLabel = []byte("xorshift64*:")
 85
 86// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used
 87// with UnmarshalBinary() to restore the state of the PRNG.
 88// MarshalBinary implements the encoding.BinaryMarshaler interface.
 89func (xs *Xorshift64Star) MarshalBinary() ([]byte, error) {
 90	b := make([]byte, 20)
 91	copy(b, marshalXorshift64StarLabel)
 92	bePutUint64(b[12:], xs.seed)
 93	return b, nil
 94}
 95
 96// errUnmarshalXorshift64Star is returned when unmarshalling fails.
 97var errUnmarshalXorshift64Star = errors.New("invalid Xorshift64* encoding")
 98
 99// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary().
100// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
101func (xs *Xorshift64Star) UnmarshalBinary(data []byte) error {
102	if len(data) != 20 || string(data[:12]) != string(marshalXorshift64StarLabel) {
103		return errUnmarshalXorshift64Star
104	}
105	xs.seed = beUint64(data[12:])
106	return nil
107}
108
109// Uint64() generates the next random uint64 value.
110func (xs *Xorshift64Star) Uint64() uint64 {
111	xs.seed ^= xs.seed >> 12
112	xs.seed ^= xs.seed << 25
113	xs.seed ^= xs.seed >> 27
114	xs.seed *= 2685821657736338717
115	return xs.seed // Operations naturally wrap around in uint64
116}
117
118// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function.
119// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing
120// for generating a million random uint64 numbers on any unix based system:
121//
122// `time gno run -expr 'benchmarkXorshift64Star()' xorshift64star.gno
123func benchmarkXorshift64Star(_iterations ...int) {
124	iterations := 1000000
125	if len(_iterations) > 0 {
126		iterations = _iterations[0]
127	}
128	xs64s := New()
129
130	for i := 0; i < iterations; i++ {
131		_ = xs64s.Uint64()
132	}
133	ufmt.Println(ufmt.Sprintf("Xorshift64*: generate %d uint64\n", iterations))
134}
135
136// The averageXorshift64Star() function is a simple benchmarking helper to demonstrate
137// the most basic statistical property of the Xorshift64* PRNG.
138func averageXorshift64Star(_iterations ...int) {
139	target := uint64(500000)
140	iterations := 1000000
141	var squares [1000000]uint64
142
143	ufmt.Println(
144		ufmt.Sprintf(
145			"Averaging %d random numbers. The average should be very close to %d.\n",
146			iterations,
147			target))
148
149	if len(_iterations) > 0 {
150		iterations = _iterations[0]
151	}
152	xs64s := New()
153
154	var average float64 = 0
155	for i := 0; i < iterations; i++ {
156		n := xs64s.Uint64()%(target*2) + 1
157		average += (float64(n) - average) / float64(i+1)
158		squares[i] = n
159	}
160
161	sum_of_squares := uint64(0)
162	// transform numbers into their squares of the distance from the average
163	for i := 0; i < iterations; i++ {
164		difference := average - float64(squares[i])
165		square := uint64(difference * difference)
166		sum_of_squares += square
167	}
168
169	ufmt.Println(ufmt.Sprintf("Xorshift64* average of %d uint64: %f\n", iterations, average))
170	ufmt.Println(ufmt.Sprintf("Xorshift64* standard deviation  : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations))))
171	ufmt.Println(ufmt.Sprintf("Xorshift64* theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12)))
172}