最近写一个demo,需要随机生成一个1024*1024长度的字符串,且是批量生成,不可避免的使用math/rand包。
话不多说,直接看代码,第一个版本:
func init () {
kinds = []uint8{48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122}
}
// 随机字符串
func Krand(size int) []byte {
result := make([]byte, size)
for i :=0; i < size; i++ {
result[i] = kinds[rand.Intn(62)]
}
return result
}
就是一个简单的for循环,调用1024*1024次,测试之后,在我的机器上测试,平均每次耗时28.912712ms。结果不尽如人意,有点慢了。
接着就是试图优化,祭出goroutine,起4个goroutine,将1024*1024分成4段分别进行初始化。按我的设想,耗时应该会大降吧,下面是代码:
func Krand2(nLen, nSlice int) []byte {
result := make([]byte, nLen)
w := sync.WaitGroup{}
w.Add(nSlice)
for s := 0; s < nSlice; s++ {
go func(b int) {
sliceLen := b + nLen/nSlice
for i := b; i < sliceLen; i++ {
result[i] = kinds[rand.Intn(62)]
}
w.Done()
}(bases[s])
}
w.Wait()
return result
}
nSlice表示将nLen分成几个片去初始化。bases是根据nSlice预先计算好的每个分片的下标起始位置。
然而,程序的运行结果却与设想恰恰相反,耗时不降反升,平均耗时60.727663ms。而且随着nSlice越大,耗时越高!
发生了什么!
查看rand.Intn的源码发现,为了方便使用,math/rand标准库提供了一个全局rand.Rand对象,可以直接使用:
var globalRand = New(&lockedSource{src: NewSource(1).(Source64)})
rand.Rand对象定义如下,有两个接口类型的成员src和s64:
type Rand struct {
src Source
s64 Source64 // non-nil if src is source64
readVal int64
readPos int8
}
标准库中对接口Source和Source64有2个实现:
${GOROOT}/src/math/rand/rand.go:374
type lockedSource struct {
***lk sync.Mutex***
src Source64
}
${GOROOT}/src/math/rand/rng.go:180
type rngSource struct {
tap int // index into vec
feed int // index into vec
vec [rngLen]int64 // current feedback register
}
而globalRand使用的实现是lockedSource,顾名思义,这个实现有一个互斥锁,这就可以解释为什么优化后的代码性能下降了。不同的goroutine在竞争这把锁,且参与竞争的goroutine越多,因竞争导致的性能损耗就越大!
明白原因之后,继续修改代码,尝试创建一个使用rngSource的rand.Rand对象,但是这个对象是非线程安全的,要避免多线程访问!修改后代码如下:
func Krand3(nLen, nSlice int) []byte {
result := make([]byte, nLen)
w := sync.WaitGroup{}
w.Add(nSlice)
for s := 0; s < nSlice; s++ {
go func(b int) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
sliceLen := b + nLen/nSlice
for i := b; i < sliceLen; i++ {
result[i] = kinds[r.Intn(62)]
}
w.Done()
}(bases[s])
}
w.Wait()
return result
}
为每个goroutine创建一个rand.Rand对象,避免多线程访问。程序运行结构平均耗时5.056477ms!
以后使用math/rand包提供的全局rand.Rand对象时,就要考虑这个互斥锁对程序的影响!
附源码:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var kinds []uint8
var bases []int
func init () {
kinds = []uint8{48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122}
}
// 随机字符串
func Krand(size int) []byte {
result := make([]byte, size)
for i :=0; i < size; i++ {
result[i] = kinds[rand.Intn(62)]
}
return result
}
func InitBase(nLen, nSlice int) {
bases = make([]int, nSlice)
for i := 0; i < nSlice; i++ {
bases[i] = nLen/nSlice * i
}
}
// 随机字符串
func Krand2(nLen, nSlice int) []byte {
result := make([]byte, nLen)
w := sync.WaitGroup{}
w.Add(nSlice)
for s := 0; s < nSlice; s++ {
go func(b int) {
sliceLen := b + nLen/nSlice
for i := b; i < sliceLen; i++ {
result[i] = kinds[rand.Intn(62)]
}
w.Done()
}(bases[s])
}
w.Wait()
return result
}
// 随机字符串
func Krand3(nLen, nSlice int) []byte {
result := make([]byte, nLen)
w := sync.WaitGroup{}
w.Add(nSlice)
for s := 0; s < nSlice; s++ {
go func(b int) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
sliceLen := b + nLen/nSlice
for i := b; i < sliceLen; i++ {
result[i] = kinds[r.Intn(62)]
}
w.Done()
}(bases[s])
}
w.Wait()
return result
}
func test(x int) (e error) {
defer func() {
if e == nil {
fmt.Println(x)
}
}()
if x > 0 {
e = nil
return
}
return fmt.Errorf("123")
}
func main(){
nLen := 1024*1024
start := time.Now()
for i := 0; i < 100; i++ {
_ = string(Krand(nLen))
}
cost := time.Since(start)
fmt.Println(cost/100)
InitBase(nLen, 4)
start = time.Now()
for i := 0; i < 100; i++ {
_ = string(Krand2(nLen, 4))
}
cost = time.Since(start)
fmt.Println(cost/100)
start = time.Now()
for i := 0; i < 100; i++ {
_ = string(Krand3(nLen, 4))
}
cost = time.Since(start)
fmt.Println(cost/100)
}