记golang标准库math/rand的一个暗坑

本文探讨了在Go语言中使用math/rand包批量生成大尺寸随机字符串的性能瓶颈及优化策略,通过对比不同实现方式,揭示了并发与互斥锁对性能的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近写一个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)
}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值