rand库锁竞争优化
前言
今天,在写随机数的生成的时候,好奇 rand 库在 golang 的实现,就翻进去看一眼的。rand.go#293 代码
var globalRand = New(&lockedSource{src: NewSource(1).(*rngSource)})
竟然是全局共享的一个 全局的 globalRand 的 对象。可以猜想到在多 goroutine 下,性能应该比较差,存在竞争的情况。
验证
随机验证一下, 验证一下
- 第一种情况,就是普通的并发情况下使用默认的 rand 库。使其共用一个 globalRand 对象。
func BenchmarkGlobalRand_test(b *testing.B) {
b.RunParallel(func(p *testing.PB) {
for p.Next() {
rand.Intn(200)
}
})
}
输出
BenchmarkGlobalRand_test
BenchmarkGlobalRand_test-8 16704190 72.53 ns/op 0 B/op 0 allocs/op
BenchmarkGlobalRand_test-8 17005326 68.90 ns/op 0 B/op 0 allocs/op
BenchmarkGlobalRand_test-8 17651659 66.54 ns/op 0 B/op 0 allocs/op
BenchmarkGlobalRand_test-8 17879901 68.07 ns/op 0 B/op 0 allocs/op
- 第二种情况,在每个 goroutine 内创建一个 rand 对象,达到不共享的效果
func BenchmarkCustomRand_test(b *testing.B) {
b.RunParallel(func(p *testing.PB) {
rd := rand.New(rand.NewSource(time.Now().Unix()))
for p.Next() {
rd.Intn(200)
}
})
}
输出
BenchmarkCustomRand_test
BenchmarkCustomRand_test-8 510722848 3.113 ns/op 0 B/op 0 allocs/op
BenchmarkCustomRand_test-8 407266668 2.877 ns/op 0 B/op 0 allocs/op
BenchmarkCustomRand_test-8 499747158 2.369 ns/op 0 B/op 0 allocs/op
BenchmarkCustomRand_test-8 509055602 2.347 ns/op 0 B/op 0 allocs/op
从上面两种情况可以看出,当不共用 globalRand 的时候,性能问题得到解决,性能大大的提升。
优化
从上面验证测试的时候, 可以看出,需要解决 rand.Rand 默认的性能问题的话,需要实现无锁,或者降低锁的竞争
无锁的方式
每个 goroutine 内创建私有的 rand.Rand 对象实现无锁。即 上面的处理方式,每次私有一个 rand.Rand 对象,实现无锁。
减少锁的竞争
要减少锁的竞争的话,思路 使用 sync.Pool 创建一个 rand.Source 的池,当多线程下并发读写的时候,在 Golang 的 G-M-P 模型下优先从当前 P 的 poolLocal 中获取,这样减少 锁的竞争
frand.go
type poolSource struct {
p *sync.Pool
}
func (s *poolSource) Int63() int64 {
v := s.p.Get()
defer s.p.Put(v)
return v.(rand.Source).Int63()
}
func (s *poolSource) Uint64() uint64 {
v := s.p.Get()
defer s.p.Put(v)
return v.(rand.Source64).Uint64()
}
func (s *poolSource) Seed(seed int64) {
v := s.p.Get()
defer s.p.Put(v)
v.(rand.Source).Seed(seed)
}
func newPoolSource() *poolSource {
s := &poolSource{}
p := &sync.Pool{New: func() interface{} {
return rand.NewSource(time.Now().Unix())
}}
s.p = p
return s
}
func New() *rand.Rand {
return rand.New(newPoolSource())
}
func NewUnsafe() *rand.Rand {
return rand.New(rand.NewSource(time.Now().Unix()))
}
frand_test.go
func BenchmarkFrandWithConcurrent_test(b *testing.B) {
rd := New()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
rd.Intn(200)
}
})
}
输出
BenchmarkGlobalRand_test
BenchmarkGlobalRand_test-8 16819393 66.43 ns/op 0 B/op 0 allocs/op
BenchmarkGlobalRand_test-8 18011631 66.14 ns/op 0 B/op 0 allocs/op
BenchmarkGlobalRand_test-8 17919164 67.06 ns/op 0 B/op 0 allocs/op
BenchmarkGlobalRand_test-8 17311258 66.95 ns/op 0 B/op 0 allocs/op
BenchmarkCustomRand_test
BenchmarkCustomRand_test-8 503757196 2.305 ns/op 0 B/op 0 allocs/op
BenchmarkCustomRand_test-8 514057155 2.346 ns/op 0 B/op 0 allocs/op
BenchmarkCustomRand_test-8 511484577 2.348 ns/op 0 B/op 0 allocs/op
BenchmarkCustomRand_test-8 512897494 2.330 ns/op 0 B/op 0 allocs/op
BenchmarkFrandWithConcurrent_test
BenchmarkFrandWithConcurrent_test-8 163811167 7.314 ns/op 0 B/op 0 allocs/op
BenchmarkFrandWithConcurrent_test-8 163572895 7.393 ns/op 0 B/op 0 allocs/op
BenchmarkFrandWithConcurrent_test-8 162728815 7.298 ns/op 0 B/op 0 allocs/op
BenchmarkFrandWithConcurrent_test-8 161633574 7.261 ns/op 0 B/op 0 allocs/op
从输出结果看 每个goroutine一个rand.Rand > sync.Pool(rand) » 默认(Global rand.Rand)
总结
所以总体的优化上两种方式
- 每个 goroutine 私有 一个 rand.Rand 对象(注意内存使用)
- 使用 sync.Pool 将 rand.Source 池 优化
上面代码实现在 github 代码
Ref: