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:

comments powered by Disqus