From 0d63302b300a758436f787d8866deb9f9f208b7d Mon Sep 17 00:00:00 2001 From: drodriguezhdez Date: Tue, 14 Apr 2020 11:44:34 +0200 Subject: [PATCH] Use random pool to generate opentracing IDs --- rand/locked_rand.go | 138 ++++++++++++++++++++++++++++++++++++++++++++ rand/num_gen.go | 7 +++ rand/pool.go | 59 +++++++++++++++++++ tracer/util.go | 26 +++++---- 4 files changed, 219 insertions(+), 11 deletions(-) create mode 100644 rand/locked_rand.go create mode 100644 rand/num_gen.go create mode 100644 rand/pool.go diff --git a/rand/locked_rand.go b/rand/locked_rand.go new file mode 100644 index 00000000..325c0f2b --- /dev/null +++ b/rand/locked_rand.go @@ -0,0 +1,138 @@ +package rand + +import ( + "math/rand" + "sync" +) + +// LockedRand implements NumberGenerator and embeds Rand that is safe for concurrent use. +type LockedRand struct { + lk sync.Mutex + r *rand.Rand +} + +// NewLockedRand creates a new LockedRand that implements all Rand functions that is safe +// for concurrent use. +func NewLockedRand(seed int64) *LockedRand { + return &LockedRand{ + r: rand.New(rand.NewSource(seed)), + } +} + +// Seed uses the provided seed value to initialize the generator to a deterministic state. +// Seed should not be called concurrently with any other Rand method. +func (lr *LockedRand) Seed(seed int64) { + lr.lk.Lock() + lr.r.Seed(seed) + lr.lk.Unlock() +} + +// TwoInt63 generates 2 random int64 without locking twice. +func (lr *LockedRand) TwoInt63() (n1, n2 int64) { + lr.lk.Lock() + n1 = lr.r.Int63() + n2 = lr.r.Int63() + lr.lk.Unlock() + return +} + +// Int63 returns a non-negative pseudo-random 63-bit integer as an int64. +func (lr *LockedRand) Int63() (n int64) { + lr.lk.Lock() + n = lr.r.Int63() + lr.lk.Unlock() + return +} + +// Uint32 returns a pseudo-random 32-bit value as a uint32. +func (lr *LockedRand) Uint32() (n uint32) { + lr.lk.Lock() + n = lr.r.Uint32() + lr.lk.Unlock() + return +} + +// Uint64 returns a pseudo-random 64-bit value as a uint64. +func (lr *LockedRand) Uint64() (n uint64) { + lr.lk.Lock() + n = lr.r.Uint64() + lr.lk.Unlock() + return +} + +// Int31 returns a non-negative pseudo-random 31-bit integer as an int32. +func (lr *LockedRand) Int31() (n int32) { + lr.lk.Lock() + n = lr.r.Int31() + lr.lk.Unlock() + return +} + +// Int returns a non-negative pseudo-random int. +func (lr *LockedRand) Int() (n int) { + lr.lk.Lock() + n = lr.r.Int() + lr.lk.Unlock() + return +} + +// Int63n returns, as an int64, a non-negative pseudo-random number in [0,n). +// It panics if n <= 0. +func (lr *LockedRand) Int63n(n int64) (r int64) { + lr.lk.Lock() + r = lr.r.Int63n(n) + lr.lk.Unlock() + return +} + +// Int31n returns, as an int32, a non-negative pseudo-random number in [0,n). +// It panics if n <= 0. +func (lr *LockedRand) Int31n(n int32) (r int32) { + lr.lk.Lock() + r = lr.r.Int31n(n) + lr.lk.Unlock() + return +} + +// Intn returns, as an int, a non-negative pseudo-random number in [0,n). +// It panics if n <= 0. +func (lr *LockedRand) Intn(n int) (r int) { + lr.lk.Lock() + r = lr.r.Intn(n) + lr.lk.Unlock() + return +} + +// Float64 returns, as a float64, a pseudo-random number in [0.0,1.0). +func (lr *LockedRand) Float64() (n float64) { + lr.lk.Lock() + n = lr.r.Float64() + lr.lk.Unlock() + return +} + +// Float32 returns, as a float32, a pseudo-random number in [0.0,1.0). +func (lr *LockedRand) Float32() (n float32) { + lr.lk.Lock() + n = lr.r.Float32() + lr.lk.Unlock() + return +} + +// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n). +func (lr *LockedRand) Perm(n int) (r []int) { + lr.lk.Lock() + r = lr.r.Perm(n) + lr.lk.Unlock() + return +} + +// Read generates len(p) random bytes and writes them into p. It +// always returns len(p) and a nil error. +// Read should not be called concurrently with any other Rand method. +func (lr *LockedRand) Read(p []byte) (n int, err error) { + lr.lk.Lock() + n, err = lr.r.Read(p) + lr.lk.Unlock() + return +} diff --git a/rand/num_gen.go b/rand/num_gen.go new file mode 100644 index 00000000..6ad94e4b --- /dev/null +++ b/rand/num_gen.go @@ -0,0 +1,7 @@ +package rand + +// NumberGenerator defines an interface to generate numbers. +type NumberGenerator interface { + Int63() int64 + TwoInt63() (int64, int64) +} diff --git a/rand/pool.go b/rand/pool.go new file mode 100644 index 00000000..c3526bad --- /dev/null +++ b/rand/pool.go @@ -0,0 +1,59 @@ +package rand + +import ( + "math/rand" + "sync/atomic" +) + +// Pool represents a pool of random number generators. +// To generate a random id, round robin through the source pool with atomic increment. +// With more and more goroutines, Pool improves the performance of Random vs naive global random +// mutex exponentially. +// Try tests with 20000 goroutines and 500 calls to observe the difference +type Pool struct { + sources []NumberGenerator + counter uint64 // used for round robin + size uint64 +} + +// see bit twiddling hacks: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 +func nextNearestPow2uint64(v uint64) uint64 { + v-- + v |= v >> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + v |= v >> 32 + v++ + return v +} + +// NewPool takes in a size and creates a pool of random id generators with size equal to next closest power of 2. +// eg: NewPool(10) returns a pool with 2^4 = 16 random sources. +func NewPool(seed int64, size uint64) *Pool { + groupsize := nextNearestPow2uint64(size) + pool := &Pool{ + size: groupsize, + sources: make([]NumberGenerator, groupsize), + } + // seed the pool + pool.seed(seed) + return pool +} + +// seed initializes the pool using a randomized sequence with given seed. +func (r *Pool) seed(seed int64) { + // init a random sequence to seed all sources + seedRan := rand.NewSource(seed) + for i := uint64(0); i < r.size; i++ { + r.sources[i] = NewLockedRand(seedRan.Int63()) + } +} + +// Pick returns a NumberGenerator from a pool of NumberGenerators +func (r *Pool) Pick() NumberGenerator { + // use round robin with fast modulus of pow2 numbers + selection := atomic.AddUint64(&r.counter, 1) & (r.size - 1) + return r.sources[selection] +} diff --git a/tracer/util.go b/tracer/util.go index 06490ed5..46975d1d 100644 --- a/tracer/util.go +++ b/tracer/util.go @@ -1,25 +1,29 @@ package tracer import ( - "math/rand" - "sync" + "runtime" "time" + + "go.undefinedlabs.com/scopeagent/rand" ) var ( - seededIDGen = rand.New(rand.NewSource(time.Now().UnixNano())) - // The golang rand generators are *not* intrinsically thread-safe. - seededIDLock sync.Mutex + randompool = rand.NewPool(time.Now().UnixNano(), uint64(max(16, runtime.NumCPU()))) ) +// max returns the larger value among a and b +func max(x, y int) int { + if x > y { + return x + } + return y +} + func randomID() uint64 { - seededIDLock.Lock() - defer seededIDLock.Unlock() - return uint64(seededIDGen.Int63()) + return uint64(randompool.Pick().Int63()) } func randomID2() (uint64, uint64) { - seededIDLock.Lock() - defer seededIDLock.Unlock() - return uint64(seededIDGen.Int63()), uint64(seededIDGen.Int63()) + n1, n2 := randompool.Pick().TwoInt63() + return uint64(n1), uint64(n2) }