Search Apps Documentation Source Content File Folder Download Copy Actions Download

xorshiftr128plus.gno

5.80 Kb ยท 186 lines
  1// Xorshiftr128+ 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 Xorshiftr128+ PRNG algorithm. This algorithm provides
  9// strong statistical performance with most seeds (just don't seed it with zeros), 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.48s
 16//	Xorshiftr128+: 1000000 Uint64 generated in 3.22s
 17//	Ratio:         x4.81 times faster than PCG
 18//
 19// Use it directly:
 20//
 21//	prng = xorshiftr128plus.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 = xorshiftr128plus.New()
 26//	prng := rand.New(source)
 27package xorshiftr128plus
 28
 29import (
 30	"errors"
 31	"math"
 32
 33	"gno.land/p/demo/entropy"
 34	"gno.land/p/nt/ufmt"
 35)
 36
 37type Xorshiftr128Plus struct {
 38	seed [2]uint64 // Seeds
 39}
 40
 41func New(seeds ...uint64) *Xorshiftr128Plus {
 42	var s1, s2 uint64
 43	seed_length := len(seeds)
 44	if seed_length < 2 {
 45		e := entropy.New()
 46		if seed_length == 0 {
 47			s1 = e.Value64()
 48			s2 = e.Value64()
 49		} else {
 50			s1 = seeds[0]
 51			s2 = e.Value64()
 52		}
 53	} else {
 54		s1 = seeds[0]
 55		s2 = seeds[1]
 56	}
 57
 58	prng := &Xorshiftr128Plus{}
 59	prng.Seed(s1, s2)
 60	return prng
 61}
 62
 63func (x *Xorshiftr128Plus) Seed(s1, s2 uint64) {
 64	if s1 == 0 && s2 == 0 {
 65		panic("Seeds must not both be zero")
 66	}
 67	x.seed[0] = s1
 68	x.seed[1] = s2
 69}
 70
 71// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding.
 72// binary.bigEndian.Uint64, copied to avoid dependency
 73func beUint64(b []byte) uint64 {
 74	_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
 75	return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
 76		uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
 77}
 78
 79// bePutUint64() encodes a uint64 into a buffer of eight bytes.
 80// binary.bigEndian.PutUint64, copied to avoid dependency
 81func bePutUint64(b []byte, v uint64) {
 82	_ = b[7] // early bounds check to guarantee safety of writes below
 83	b[0] = byte(v >> 56)
 84	b[1] = byte(v >> 48)
 85	b[2] = byte(v >> 40)
 86	b[3] = byte(v >> 32)
 87	b[4] = byte(v >> 24)
 88	b[5] = byte(v >> 16)
 89	b[6] = byte(v >> 8)
 90	b[7] = byte(v)
 91}
 92
 93// A label to identify the marshalled data.
 94var marshalXorshiftr128PlusLabel = []byte("xorshiftr128+:")
 95
 96// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used
 97// with UnmarshalBinary() to restore the state of the PRNG.
 98// MarshalBinary implements the encoding.BinaryMarshaler interface.
 99func (xs *Xorshiftr128Plus) MarshalBinary() ([]byte, error) {
100	b := make([]byte, 30)
101	copy(b, marshalXorshiftr128PlusLabel)
102	bePutUint64(b[14:], xs.seed[0])
103	bePutUint64(b[22:], xs.seed[1])
104	return b, nil
105}
106
107// errUnmarshalXorshiftr128Plus is returned when unmarshalling fails.
108var errUnmarshalXorshiftr128Plus = errors.New("invalid Xorshiftr128Plus encoding")
109
110// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary().
111// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
112func (xs *Xorshiftr128Plus) UnmarshalBinary(data []byte) error {
113	if len(data) != 30 || string(data[:14]) != string(marshalXorshiftr128PlusLabel) {
114		return errUnmarshalXorshiftr128Plus
115	}
116	xs.seed[0] = beUint64(data[14:])
117	xs.seed[1] = beUint64(data[22:])
118	return nil
119}
120
121func (x *Xorshiftr128Plus) Uint64() uint64 {
122	x0 := x.seed[0]
123	x1 := x.seed[1]
124	x.seed[0] = x1
125	x0 ^= x0 << 23
126	x0 ^= x0 >> 17
127	x0 ^= x1
128	x.seed[1] = x0 + x1
129	return x.seed[1]
130}
131
132// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function.
133// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing
134// for generating a million random uint64 numbers on any unix based system:
135//
136// `time gno run -expr 'benchmarkXorshiftr128Plus()' xorshiftr128plus.gno
137func benchmarkXorshiftr128Plus(_iterations ...int) {
138	iterations := 1000000
139	if len(_iterations) > 0 {
140		iterations = _iterations[0]
141	}
142	xs128p := New()
143
144	for i := 0; i < iterations; i++ {
145		_ = xs128p.Uint64()
146	}
147	ufmt.Println(ufmt.Sprintf("Xorshiftr128Plus: generate %d uint64\n", iterations))
148}
149
150// The averageXorshiftr128Plus() function is a simple benchmarking helper to demonstrate
151// the most basic statistical property of the Xorshiftr128+ PRNG.
152func averageXorshiftr128Plus(_iterations ...int) {
153	target := uint64(500000)
154	iterations := 1000000
155	var squares [1000000]uint64
156
157	ufmt.Println(
158		ufmt.Sprintf(
159			"Averaging %d random numbers. The average should be very close to %d.\n",
160			iterations,
161			target))
162
163	if len(_iterations) > 0 {
164		iterations = _iterations[0]
165	}
166	xs128p := New()
167
168	var average float64 = 0
169	for i := 0; i < iterations; i++ {
170		n := xs128p.Uint64()%(target*2) + 1
171		average += (float64(n) - average) / float64(i+1)
172		squares[i] = n
173	}
174
175	sum_of_squares := uint64(0)
176	// transform numbers into their squares of the distance from the average
177	for i := 0; i < iterations; i++ {
178		difference := average - float64(squares[i])
179		square := uint64(difference * difference)
180		sum_of_squares += square
181	}
182
183	ufmt.Println(ufmt.Sprintf("Xorshiftr128+ average of %d uint64: %f\n", iterations, average))
184	ufmt.Println(ufmt.Sprintf("Xorshiftr128+ standard deviation  : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations))))
185	ufmt.Println(ufmt.Sprintf("Xorshiftr128+ theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12)))
186}