mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-27 10:47:35 +00:00
util/rands: add a cheap non-escaping rand type
math/rand and the current v2 proposal retain a source interface that means they always heap allocate until some future compiler changes this constraint. This type is one that can be used on-stack for use cases like a cheap in-place shuffle with an on-stack random generator. Updates #17243 Signed-off-by: James Tucker <james@tailscale.com>
This commit is contained in:
parent
5595b61b96
commit
0207c19137
@ -416,6 +416,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
|
||||
golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
|
||||
golang.org/x/exp/maps from tailscale.com/wgengine/magicsock+
|
||||
golang.org/x/exp/rand from tailscale.com/util/rands
|
||||
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
|
||||
|
66
util/rands/cheap.go
Normal file
66
util/rands/cheap.go
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package rands
|
||||
|
||||
import (
|
||||
exprand "golang.org/x/exp/rand"
|
||||
)
|
||||
|
||||
// A Rand is a source of random numbers. It is extremely cheap to create and
|
||||
// seed on the stack, and always uses the PCG random number generator.
|
||||
type Rand struct {
|
||||
src exprand.PCGSource
|
||||
}
|
||||
|
||||
// NewRand returns a new Rand with the given seed.
|
||||
func NewRand(seed uint64) Rand {
|
||||
var r Rand
|
||||
r.Seed(seed)
|
||||
return r
|
||||
}
|
||||
|
||||
// Seed uses the provided seed value to reinitialize the generator to a
|
||||
// deterministic state.
|
||||
// Seed should not be called concurrently with any other Rand method.
|
||||
func (r *Rand) Seed(seed uint64) {
|
||||
r.src.Seed(seed)
|
||||
}
|
||||
|
||||
// Uint64 returns a pseudo-random 64-bit integer as a uint64.
|
||||
func (r *Rand) Uint64() uint64 { return r.src.Uint64() }
|
||||
|
||||
const maxUint64 = (1 << 64) - 1
|
||||
|
||||
// Uint64n returns, as a uint64, a pseudo-random number in [0,n).
|
||||
// It is guaranteed more uniform than taking a Source value mod n
|
||||
// for any n that is not a power of 2.
|
||||
func (r *Rand) Uint64n(n uint64) uint64 {
|
||||
if n&(n-1) == 0 { // n is power of two, can mask
|
||||
if n == 0 {
|
||||
panic("invalid argument to Uint64n")
|
||||
}
|
||||
return r.Uint64() & (n - 1)
|
||||
}
|
||||
// If n does not divide v, to avoid bias we must not use
|
||||
// a v that is within maxUint64%n of the top of the range.
|
||||
v := r.Uint64()
|
||||
if v > maxUint64-n { // Fast check.
|
||||
ceiling := maxUint64 - maxUint64%n
|
||||
for v >= ceiling {
|
||||
v = r.Uint64()
|
||||
}
|
||||
}
|
||||
|
||||
return v % n
|
||||
}
|
||||
|
||||
// Intn returns, as an int, a non-negative pseudo-random number in [0,n).
|
||||
// It panics if n <= 0.
|
||||
func (r *Rand) Intn(n int) int {
|
||||
if n <= 0 {
|
||||
panic("invalid argument to Intn")
|
||||
}
|
||||
// TODO: Avoid some 64-bit ops to make it more efficient on 32-bit machines.
|
||||
return int(r.Uint64n(uint64(n)))
|
||||
}
|
196
util/rands/cheap_test.go
Normal file
196
util/rands/cheap_test.go
Normal file
@ -0,0 +1,196 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package rands
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
exprand "golang.org/x/exp/rand"
|
||||
)
|
||||
|
||||
var (
|
||||
seed uint64 = 8729831
|
||||
numDraw = 100
|
||||
numGoroutines = 5000
|
||||
)
|
||||
|
||||
type workerPool struct {
|
||||
job chan func()
|
||||
res chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func (p *workerPool) Close() {
|
||||
close(p.job)
|
||||
p.wg.Wait()
|
||||
}
|
||||
|
||||
func newWorkerPool() *workerPool {
|
||||
pool := workerPool{
|
||||
job: make(chan func(), 2<<20),
|
||||
res: make(chan struct{}, 2<<20),
|
||||
wg: sync.WaitGroup{},
|
||||
}
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
pool.wg.Add(1)
|
||||
go func() {
|
||||
defer pool.wg.Done()
|
||||
for f := range pool.job {
|
||||
f()
|
||||
pool.res <- struct{}{}
|
||||
}
|
||||
}()
|
||||
}
|
||||
return &pool
|
||||
}
|
||||
|
||||
var stdPool = sync.Pool{
|
||||
New: func() any {
|
||||
return rand.New(rand.NewSource(int64(seed)))
|
||||
},
|
||||
}
|
||||
|
||||
var expPool = sync.Pool{
|
||||
New: func() any {
|
||||
return exprand.New(exprand.NewSource(seed))
|
||||
},
|
||||
}
|
||||
|
||||
func BenchmarkStd(b *testing.B) {
|
||||
pool := newWorkerPool()
|
||||
defer pool.Close()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
pool.job <- func() {
|
||||
rand.Seed(int64(seed))
|
||||
for i := 0; i < numDraw; i++ {
|
||||
rand.Intn(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
<-pool.res
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPCG(b *testing.B) {
|
||||
pool := newWorkerPool()
|
||||
defer pool.Close()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
pool.job <- func() {
|
||||
exprand.Seed(seed)
|
||||
for i := 0; i < numDraw; i++ {
|
||||
exprand.Intn(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
<-pool.res
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStdPool(b *testing.B) {
|
||||
pool := newWorkerPool()
|
||||
defer pool.Close()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
pool.job <- func() {
|
||||
r := stdPool.Get().(*rand.Rand)
|
||||
defer stdPool.Put(r)
|
||||
|
||||
r.Seed(int64(seed))
|
||||
for i := 0; i < numDraw; i++ {
|
||||
r.Intn(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
<-pool.res
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPCGPool(b *testing.B) {
|
||||
pool := newWorkerPool()
|
||||
defer pool.Close()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
pool.job <- func() {
|
||||
r := expPool.Get().(*exprand.Rand)
|
||||
defer expPool.Put(r)
|
||||
|
||||
r.Seed(seed)
|
||||
for i := 0; i < numDraw; i++ {
|
||||
r.Intn(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
<-pool.res
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLocalStd(b *testing.B) {
|
||||
pool := newWorkerPool()
|
||||
defer pool.Close()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
pool.job <- func() {
|
||||
r := rand.New(rand.NewSource(int64(seed)))
|
||||
for i := 0; i < numDraw; i++ {
|
||||
r.Intn(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
<-pool.res
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLocalPCG(b *testing.B) {
|
||||
pool := newWorkerPool()
|
||||
defer pool.Close()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
pool.job <- func() {
|
||||
r := exprand.New(exprand.NewSource(seed))
|
||||
for i := 0; i < numDraw; i++ {
|
||||
r.Intn(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
<-pool.res
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStackRand(b *testing.B) {
|
||||
pool := newWorkerPool()
|
||||
defer pool.Close()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
pool.job <- func() {
|
||||
r := NewRand(seed)
|
||||
for i := 0; i < numDraw; i++ {
|
||||
r.Intn(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
<-pool.res
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackRandNoAllocs(t *testing.T) {
|
||||
seed := rand.Uint64()
|
||||
if n := testing.AllocsPerRun(1000, func() {
|
||||
r := NewRand(seed)
|
||||
_ = r.Intn(100)
|
||||
}); n > 0 {
|
||||
t.Errorf("Rand got %v allocs per run", n)
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user