前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >慎重使用默认随机函数

慎重使用默认随机函数

作者头像
数据小冰
发布2022-08-15 14:58:19
5210
发布2022-08-15 14:58:19
举报
文章被收录于专栏:数据小冰

在看rpc源码的时候,看到产生随机数的方法是调用r= rand.New(rand.NewSource(time.Now().Unix())),而小编通常使用的都是rand.Intxx,这两者有什么不一样呢?好奇查看rand.Intxx的实现,发现它用到了锁。我们知道锁会引起性能问题,那为啥产生随机数要加锁呢?

代码语言:javascript
复制
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产生随机数,需要获取锁,这个影响有多大呢?需要通过程来验证,已有人做了相关的验证,见引用资料,本文中的例子可以看做是对引用的翻译提炼总结。话不多说,先上代码。

代码语言:javascript
复制
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对象。改进后代码如下。

代码语言:javascript
复制
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和计算的压力,能不能对这里进行优化,可以的。作者做了一点小改动,改动地方有两处。

代码语言:javascript
复制
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]

Reference

[1]

The Hidden Dangers of Default Rand: https://blog.sgmansfield.com/2016/01/the-hidden-dangers-of-default-rand/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 性能影响
  • 改进优化
  • 进一步改进优化
    • Reference
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档