这个问题要求“最快和最简单的方法”。让我们来解决这个问题。我们将以迭代的方式到达我们的最终,最快的代码。可以在答案的结尾找到对每个迭代的基准。
所有的解决方案和基准代码都可以在Go Playground上找到。Playground上的代码是一个测试文件,而不是一个可执行文件。你必须将它保存到一个名为XX_test.go的文件,并运行它用go test -bench ..
I.改进
创世纪
作为一个提醒,我们正在改进的最初的,一般的解决方案是:
func init() { rand.Seed(time.Now().UnixNano()) } var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func RandStringRunes(n int) string { b := make([]rune,n) for i := range b { b[i] = letterRunes[rand.Intn(len(letterRunes))] } return string(b) }
2.字节
如果选择字符并组合随机字符串只包含英语字母表的大写字母和小写字母,我们可以处理字节,因为英语字母表字母映射到UTF-8编码的字节1对1是Go如何存储字符串)。
所以,而不是:
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
我们可以用:
var letters = []bytes("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
或更好:
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
现在这已经是一个很大的改进:我们可以实现它是一个const(有字符串常量,但there are no slice constants)。作为额外的增益,表达式len(letters)也将是一个常数! (如果s是字符串常量,则表达式len(s)是常量。)
和什么成本?没有什么。字符串可以索引其索引其字节,完美,正是我们想要的。
我们的下一个目的地如下所示:
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" func RandStringBytes(n int) string { b := make([]byte,n) for i := range b { b[i] = letterBytes[rand.Intn(len(letterBytes))] } return string(b) }
3.剩余
以前的解决方案得到一个随机数字,通过呼叫rand.Intn()
指定一个随机字母,它代表Rand.Intn()
,它代表Rand.Int31n()
。
这比rand.Int63()
慢得多,这产生具有63个随机位的随机数。
所以我们可以简单地调用rand.Int63()并使用除以len(letterBytes)后的余数:
func RandStringBytesRmndr(n int) string { b := make([]byte,n) for i := range b { b[i] = letterBytes[rand.Int63() % int64(len(letterBytes))] } return string(b) }
这工作和显着更快,缺点是所有字母的概率不会完全相同(假设rand.Int63()产生所有63位数的概率相等)。尽管失真非常小,因为字母52的数量远小于1 <63-1,因此在实践中这是完全精细的。 为了使这更容易理解:让我们假设你想要一个在0..5范围内的随机数。使用3个随机位,这将产生具有双概率的数字0..1,而不是范围2..5。使用5个随机比特,范围0..1中的数字将以6/32的概率发生,而数字在范围2..5中以5/32的概率发生,现在更接近期望的。增加位数使其不那么重要,当达到63位时,它是可以忽略的。 4.掩蔽 基于前面的解决方案,我们可以通过使用尽可能多的随机数的最低位来保持字母的平均分布,因为需要很多位来表示字母数。例如,如果我们有52个字母,它需要6位来表示它:52 = 110100b。因此,我们将只使用rand.Int63()返回的数字的最低6位。为了保持字母的均匀分布,我们只有“接受”数字,如果它落在0..len(letterBytes)-1的范围内。如果最低位较大,我们丢弃它并查询一个新的随机数。 请注意,最低位大于或等于len(letterBytes)的机会通常小于0.5(平均为0.25),这意味着即使这种情况发生,重复此“罕见”情况会减少没有找到好数字的机会。在n重复之后,我们基石没有良好指数的机会比pow(0.5,n)小得多,这只是一个较高的估计。在52个字母的情况下,6个最低位不好的可能性仅为(64-52)/ 64 = 0.19;这意味着例如在10次重复之后没有良好数字的机会是1e-8。 所以这里是解决方案:
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const ( letterIdxBits = 6 // 6 bits to represent a letter index letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits,as many as letterIdxBits ) func RandStringBytesMask(n int) string { b := make([]byte,n) for i := 0; i < n; { if idx := int(rand.Int63() & letterIdxMask); idx < len(letterBytes) { b[i] = letterBytes[idx] i++ } } return string(b) }
5.掩蔽改进
前面的解决方案只使用rand.Int63()返回的63个随机位的最低6位。这是一个浪费,因为获得随机位是我们的算法中最慢的部分。
如果我们有52个字母,这意味着6位代码一个字母索引。因此63个随机位可以指定63/6 = 10个不同的字母索引。让我们使用所有这些10:
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const ( letterIdxBits = 6 // 6 bits to represent a letter index letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits,as many as letterIdxBits letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits ) func RandStringBytesMaskImpr(n int) string { b := make([]byte,n) // A rand.Int63() generates 63 random bits,enough for letterIdxMax letters! for i,cache,remain := n-1,rand.Int63(),letterIdxMax; i >= 0; { if remain == 0 { cache,remain = rand.Int63(),letterIdxMax } if idx := int(cache & letterIdxMask); idx < len(letterBytes) { b[i] = letterBytes[idx] i-- } cache >>= letterIdxBits remain-- } return string(b) }
6.来源
Masking Improved是相当不错,不太多,我们可以改进。我们可以,但不值得复杂。
现在让我们找到一些改进的东西。随机数的来源。
有一个crypto/rand
包提供一个Read(b []byte)
功能,所以我们可以使用它来获得尽可能多的字节与一个调用尽可能多的我们需要。这在性能方面没有帮助,因为crypto / rand实现了一个密码安全的伪随机数生成器,所以它慢得多。
所以让我们坚持数学/ rand包。 rand.Rand使用rand.Source
作为随机位的源。 rand.Source是一个接口,它指定了一个Int63()int64方法:这是我们最新解决方案中唯一需要和使用的东西。
所以我们不需要一个rand.Rand(显式或全局,共享一个rand包),一个rand.Source是完全足够我们:
var src = rand.NewSource(time.Now().UnixNano()) func RandStringBytesMaskImprSrc(n int) string { b := make([]byte,n) // A src.Int63() generates 63 random bits,enough for letterIdxMax characters! for i,src.Int63(),remain = src.Int63(),letterIdxMax } if idx := int(cache & letterIdxMask); idx < len(letterBytes) { b[i] = letterBytes[idx] i-- } cache >>= letterIdxBits remain-- } return string(b) }
还要注意,最后一个解决方案不需要你初始化(种子)math / rand包的全局Rand,因为没有使用(和我们的rand.Source正确初始化/种子)。
II。基准
好的,让我们对不同的解决方案进行评测。
BenchmarkRunes 1000000 1703 ns/op BenchmarkBytes 1000000 1328 ns/op BenchmarkBytesRmndr 1000000 1012 ns/op BenchmarkBytesMask 1000000 1214 ns/op BenchmarkBytesMaskImpr 5000000 395 ns/op BenchmarkBytesMaskImprSrc 5000000 303 ns/op
只需从符文切换到字节,我们立即就有22%的性能提升。
摆脱rand.Intn()和使用rand.Int63(),而是提供了另一个24%的提升。
屏蔽(在大索引的情况下重复)减慢一点(由于重复调用):-20%…
但是当我们使用所有(或大多数)63个随机位(来自一个rand.Int63()调用的10个索引):加速3.4倍。
最后,如果我们用rand.Source而不是rand.Rand结算,我们再次获得23%。
比较final和初始解决方案:RandStringBytesMaskImprSrc()是RandStringRunes()的5.6倍。