Golang math/rand 源码剖析&避坑指南
Posted 恋喵大鲤鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang math/rand 源码剖析&避坑指南相关的知识,希望对你有一定的参考价值。
1.前言
Go 版本为 go 1.17。
go version go1.17 darwin/amd64
本文以type rand struct
为切入点,看下 Go 伪随机数的实现原理。
// A Rand is a source of random numbers.
type Rand struct {
src Source
s64 Source64 // non-nil if src is source64
// readVal contains remainder of 63-bit integer used for bytes
// generation during most recent Read call.
// It is saved so next Read call can start where the previous
// one finished.
readVal int64
// readPos indicates the number of low-order bytes of readVal
// that are still valid.
readPos int8
}
2.剖析
伪随机数如果每次种子相同,那么生成的随机序列也是相同的。下面通过赋予不同的种子创建一个随机数发生器。
r := rand.New(rand.NewSource(time.Now().UnixNano()))
r.Intn(62) // 生成 [0, n) 内的整数
我们以 rand.Intn()
为例,看下随机数的实现。
// Intn returns, as an int, a non-negative pseudo-random number in the half-open interval [0,n).
// It panics if n <= 0.
func (r *Rand) Intn(n int) int {
if n <= 0 {
panic("invalid argument to Intn")
}
if n <= 1<<31-1 {
return int(r.Int31n(int32(n)))
}
return int(r.Int63n(int64(n)))
}
我传入的是 62 小于,所以调用的是r.Int31n()
。
// Int31n returns, as an int32, a non-negative pseudo-random number in the half-open interval [0,n).
// It panics if n <= 0.
func (r *Rand) Int31n(n int32) int32 {
if n <= 0 {
panic("invalid argument to Int31n")
}
if n&(n-1) == 0 { // n is power of two, can mask
return r.Int31() & (n - 1)
}
max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
v := r.Int31()
for v > max {
v = r.Int31()
}
return v % n
}
因为我们的 n=62 不是 2 的幂,所以走的是下面的逻辑。其中这个 max 操作需要明白其作用。
max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
max 就是将 int32 范围 [0, (1 << 31) - 1] 内最后取模不能覆盖 [0, n) 的部分去掉,保证 [0, n) 内各个整数出现的概率相同。看下几个具体的值就明白了。
var n int32 = 62
tail := (1<<31) % uint32(n)
max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
fmt.Println(tail) // 2
fmt.Println(max) // 2147483645
fmt.Println(max % n) // 61
再看下真正产生随机数的函数r.Int31()
。
// Int31 returns a non-negative pseudo-random 31-bit integer as an int32.
func (r *Rand) Int31() int32 { return int32(r.Int63() >> 32) }
其又调用的是r.Int63()
,取高 31 位作为 int32 的随机值。
// Int31 returns a non-negative pseudo-random 31-bit integer as an int32.
func (r *Rand) Int31() int32 { return int32(r.Int63() >> 32) }
其又调用的是r.src.Int63()
。我们先看下type Source interface
的定义。
// A Source represents a source of uniformly-distributed
// pseudo-random int64 values in the range [0, 1<<63).
type Source interface {
Int63() int64
Seed(seed int64)
}
我们初始化 Rand 的时候,通过rand.New(rand.NewSource(seed))
创建,看下rand.New()
的实现。
// New returns a new Rand that uses random values from src
// to generate other random values.
func New(src Source) *Rand {
s64, _ := src.(Source64)
return &Rand{src: src, s64: s64}
}
可见 Rand 使用的是rand.NewSource()
传入的 Source,看下rand.NewSource()
的实现。
// NewSource returns a new pseudo-random Source seeded with the given value.
// Unlike the default Source used by top-level functions, this source is not
// safe for concurrent use by multiple goroutines.
func NewSource(seed int64) Source {
var rng rngSource
rng.Seed(seed)
return &rng
}
可见 Source 的实际类型是 rngSource,实现了接口 Source,其定义如下:
type rngSource struct {
tap int // index into vec
feed int // index into vec
vec [rngLen]int64 // current feedback register
}
我们再看下rngSource.Int63()
的实现。
// Int63 returns a non-negative pseudo-random 63-bit integer as an int64.
func (rng *rngSource) Int63() int64 {
return int64(rng.Uint64() & rngMask)
}
其中 rngMask 的定义如下,表示 Int64 的最大值,作用是作为掩码。
rngMax = 1 << 63
rngMask = rngMax - 1
至此,我们找到了随机数生成的两个核心函数,一个是根据种子初始化数组 rngSource.vec
的函数rngSource.Seed()
,一个是从数组 rngSource.vec
取出随机数的rngSource.Uint64()
。
3.核心函数
我们看下随机数的真正生成函数rngSource.Uint64()
。
// Uint64 returns a non-negative pseudo-random 64-bit integer as an uint64.
func (rng *rngSource) Uint64() uint64 {
rng.tap--
if rng.tap < 0 {
rng.tap += rngLen
}
rng.feed--
if rng.feed < 0 {
rng.feed += rngLen
}
x := rng.vec[rng.feed] + rng.vec[rng.tap]
rng.vec[rng.feed] = x
return uint64(x)
}
实际上,我们在调用Intn(), Int31n(), Int63(), Int63n()
等其他函数,最终调用到都是函数rngSource.Uint64()
。可以看到每次调用就是利用 rng.feed, rng.tap 从 rng.vec 中取到两个值相加的结果返回,同时这个结果又重新放入 rng.vec。这么做的目的显而易见,让随机数更加丰富随机,而不是仅局限于 rng.vec 数组中的值。
另外 rng.tap、rng.feed 和 rng.vec 的初始化工作是在函数rngSource.Seed()
中完成的。
// Seed uses the provided seed value to initialize the generator to a deterministic state.
func (rng *rngSource) Seed(seed int64) {
rng.tap = 0
rng.feed = rngLen - rngTap
seed = seed % int32max
if seed < 0 {
seed += int32max
}
if seed == 0 {
seed = 89482311
}
x := int32(seed)
for i := -20; i < rngLen; i++ {
x = seedrand(x)
if i >= 0 {
var u int64
u = int64(x) << 40
x = seedrand(x)
u ^= int64(x) << 20
x = seedrand(x)
u ^= int64(x)
u ^= rngCooked[i]
rng.vec[i] = u
}
}
}
相关常量定义如下。
const (
rngLen = 607
rngTap = 273
rngMax = 1 << 63
rngMask = rngMax - 1
int32max = (1 << 31) - 1
)
其中函数seedrand()
也需要关注下,其是完成对 seed 的变换。这也导致了相同的 seed, 最终设置到 rng.vec 里面的值是相同的,通过rngSource.Uint64()
取出的也是相同的值。
// seed rng x[n+1] = 48271 * x[n] mod (2**31 - 1)
func seedrand(x int32) int32 {
const (
A = 48271
Q = 44488
R = 3399
)
hi := x / Q
lo := x % Q
x = A*lo - R*hi
if x < 0 {
x += int32max
}
return x
}
至此我们大值了解了 math/rand 的随机数的生成过程。
4.需要避开的坑
通过上面对 math/rand 的分析,我们应该知道使用时需要避开的坑。
(1)相同种子,每次运行的结果是一样的。 因为随机数是从 rng.vec 数组中取出来的,这个数组是根据种子生成的,相同的种子生成的 rng.vec 数组是相同的。
(2)不同种子,每次运行的结果可能一样。 因为根据种子生成 rng.vec 数组时会有一个取模的操作,模后的结果可能相同,导致 rng.vec 数组相同。
(3)rand.New 初始化出来的 rand 不是并发安全的。 因为每次利用 rng.feed, rng.tap 从 rng.vec 中取到随机值后会将随机值重新放入 rng.vec。如果想并发安全,可以使用全局的随机数发生器 rand.globalRand。
(4)不同种子,随机序列发生碰撞的概率高于单个碰撞概率的乘积。 这是因为存在生日问题。
比如我要随机从数字字母集(62个字符)中获取长度为 6 的邀请码,种子使用用户ID,如果生成 100W 个邀请码,假设前 100W 一个都不重复,那么下一个重复的概率是((1/62)^6 * 100W)≈1/5.6W
,冲突率已经到了在万分之一的概率,远大于想象中的(1/62)^6
。
参考文献
以上是关于Golang math/rand 源码剖析&避坑指南的主要内容,如果未能解决你的问题,请参考以下文章