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


参考文献

CSDN.一文完全掌握 Go math/rand
CSDN.记录使用 Golang math/rand 随机数遇到的坑

以上是关于Golang math/rand 源码剖析&避坑指南的主要内容,如果未能解决你的问题,请参考以下文章

深度解密 Go math/rand

golang之math/rand随机数

Golang之math/rand,双色球预测

GoLang 之旅

记录使用 Golang math/rand 随机数遇到的坑

记录使用 Golang math/rand 随机数遇到的坑