在看rpc源码的时候,看到产生随机数的方法是调用r= rand.New(rand.NewSource(time.Now().Unix())),而小编通常使用的都是rand.Intxx,这两者有什么不一样呢?好奇查看rand.Intxx的实现,发现它用到了锁。我们知道锁会引起性能问题,那为啥产生随机数要加锁呢?
var globalRand = New(&lockedSource{src: NewSource(1).(*rngSource)})
func Int63() int64 { return globalRand.Int63() }
func Uint32() uint32 { return globalRand.Uint32() }
type lockedSource struct {
lk sync.Mutex
src *rngSource
}
func (r *lockedSource) Int63() (n int64) {
r.lk.Lock()
n = r.src.Int63()
r.lk.Unlock()
return
}
func (r *lockedSource) Uint64() (n uint64) {
r.lk.Lock()
n = r.src.Uint64()
r.lk.Unlock()
return
}
rand.Intxxx是开箱即用的,通过上面的代码可以看到,它们是对globalRand.xx做了封装,globalRand是一个全局变量,它是一个lockedSource类型,lockedSource在产生Int63、Uint64之类的函数时,都需要获取锁。
产生随机数的时候,一次生成一个数字,然后成为下一个数字的基础。这个是不能安全地并发访问的,因此需要一个锁来保证串行化。
使用rand.Intxx产生随机数,需要获取锁,这个影响有多大呢?需要通过程来验证,已有人做了相关的验证,见引用资料,本文中的例子可以看做是对引用的翻译提炼总结。话不多说,先上代码。
package main
import (
"fmt"
"math/rand"
"os"
"runtime/pprof"
"sync"
"time"
)
var letters = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randData(n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return b
}
const numRuns = 10
func main() {
start := time.Now()
for i := 0; i < numRuns; i++ {
do()
}
total := time.Since(start)
perRun := float64(total) / numRuns
fmt.Printf("Time per run: %fns\n", perRun)
// Now generate the pprof data for a single run
f, err := os.Create("rand_default.prof")
if err != nil {
panic(err.Error())
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
do()
}
const numRoutines = 10
func do() {
start := make(chan struct{})
comm := make(chan []byte)
var read, write sync.WaitGroup
read.Add(numRoutines)
write.Add(numRoutines)
for i := 0; i < numRoutines; i++ {
go func() {
<-start
for j := 1; j < 10000; j++ {
comm <- randData(rand.Intn(j))
}
write.Done()
}()
go func() {
var sum int
<-start
for c := range comm {
sum += len(c)
}
//fmt.Println(sum)
read.Done()
}()
}
close(start)
write.Wait()
close(comm)
read.Wait()
}
核心函数是do函数,它的功能是开启10个goroutine产生随机数,并将随机数发送一个有缓冲的channel中,在开启10个goroutine并发的从channel中取走随机数,一共产生10万个随机数。整个逻辑是一个生产者-消费者模型。作者也在文中做了说明。
下面看运行测试结果,本文测试使用是Go1.14版本,在我的2.3 GHz 双核Intel Core i5处理器上,平均单次运行时间约为16.37秒
下面是单次运行do函数,抓取的cpu的采样文件生成的pprof图。产生随机数10.88秒的时间中,获取锁和释放锁占用的时间高达5.74秒和2.64秒,也就是有(5.74+2.64)/10.88=0.77,即有77%的时间浪费在锁的争用上。
有什么改进优化措施吗,上面的时间大量浪费在了global锁的竞争上,所有的goroutine都在抢一把锁,那如果goroutine使用各自的锁,大家互相不干扰,那锁的影响就下降了。对,这种思路就是用本文开头的rand.New产生一个*rand.Rand对象,每个生产者一个,不同生产者是不同的rand.Rand对象。改进后代码如下。
package main
import (
"fmt"
"math/rand"
"os"
"runtime/pprof"
"sync"
"time"
)
var letters = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randData(r *rand.Rand, n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = letters[r.Intn(len(letters))]
}
return b
}
const numRuns = 10
func main() {
start := time.Now()
for i := 0; i < numRuns; i++ {
do()
}
total := time.Since(start)
perRun := float64(total) / numRuns
fmt.Printf("Time per run: %fns\n", perRun)
// Now generate the pprof data for a single run
f, err := os.Create("rand_optimized.prof")
if err != nil {
panic(err.Error())
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
do()
}
const numRoutines = 10
func do() {
start := make(chan struct{})
comm := make(chan []byte)
var read, write sync.WaitGroup
read.Add(numRoutines)
write.Add(numRoutines)
for i := 0; i < numRoutines; i++ {
go func() {
r := rand.New(rand.NewSource(time.Now().Unix()))
<-start
for j := 1; j < 10000; j++ {
comm <- randData(r, r.Intn(j))
}
write.Done()
}()
go func() {
var sum int
<-start
for c := range comm {
sum += len(c)
}
//fmt.Println(sum)
read.Done()
}()
}
close(start)
write.Wait()
close(comm)
read.Wait()
}
平均运行时间大约为1.59秒,与前面的相比,大约有10X倍的性能提升。效果是非常明显的。
从下面的cpu pprof图可以看到,已经没有锁的争用消耗了。主要的时间耗费在(*Rand) Int31n上。
在前面的程序中,每次调用randData,都要make一个切片,然后赋值,这会造成gc和计算的压力,能不能对这里进行优化,可以的。作者做了一点小改动,改动地方有两处。
var rData []byte
func init() {
r := rand.New(rand.NewSource(time.Now().Unix()))
rData = randData(r, 10000)
}
go func() {
r := rand.New(rand.NewSource(time.Now().Unix()))
<-start
for j := 1; j < 10000; j++ {
comm <- rData[:r.Intn(j)]
}
write.Done()
}()
平均运行时间大约为0.028秒,与最初的版本相比,有几百倍的提升。
下面是进一步改进优化后的cpu pprof图,可以看到,处理时间下降到了几十毫秒的级别。
The Hidden Dangers of Default Rand[1]
[1]
The Hidden Dangers of Default Rand: https://blog.sgmansfield.com/2016/01/the-hidden-dangers-of-default-rand/