Paul的解决方案提供了一个 简单的 通用解决方案。
问题要求 “最快,最简单的方法” 。让我们也讨论 最快的
部分。我们将以迭代的方式得出最终的最快的代码。对每个迭代进行基准测试可以在答案的结尾处找到。
所有解决方案和基准代码都可以在GoPlayground上找到。Playground上的代码是测试文件,而不是可执行文件。您必须将其保存到一个名为的文件中
XX_test.go,然后使用
go test -bench . -benchmem
前言 :
如果只需要随机字符串,最快的解决方案不是首选解决方案。为此,保罗的解决方案是完美的。这就是性能确实重要。尽管前两个步骤( Bytes 和
Remainder )可能是一个可以接受的折衷方案:它们确实将性能提高了大约50%(请参阅 II.Benchmark
部分中的确切数字),并且不会显着增加复杂性。
话虽如此,即使您不需要最快的解决方案,通读此答案也可能是冒险和有教育意义的。
一,改进
1.创世记(符文)
提醒一下,我们正在改进的原始通用解决方案是:
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"
现在这已经是一个很大的改进:我们可以实现为a
const(有
string常量,但没有切片常量)。作为额外的收获,表达式
len(letters)也将是
const!(
len(s)如果
s为字符串常量,则表达式为常量。)
而且要花多少钱?没事
string可以对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比range两倍的概率
2..5。使用5个随机位,范围内的数字
0..1将以
6/32概率出现,范围内的数字
2..5将以
5/32概率出现,现在更接近所需值。增加位数会使此意义降低,当达到63位时,可以忽略不计。
4.遮罩
在前面的解决方案的基础上,我们可以通过使用与代表字母数量所需的数量一样多的随机数最低位来维持字母的均等分布。因此,例如,如果我们有52个字母,则需要6位才能表示它:`52
110100b
。因此,我们将仅使用所返回数字的最低6位rand.Int63()
。为了保持字母的均等分布,我们仅在数字落入范围内时才“接受”该数字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.改进了遮罩
先前的解决方案仅使用由返回的63个随机位中的最低6位
rand.Int63()。这是浪费,因为获取随机位是我们算法中最慢的部分。
如果我们有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.来源
改进 的 遮罩效果 非常好,我们可以对其进行改进。我们可以,但不值得如此复杂。
现在让我们找到其他需要改进的地方。 随机数的来源。
有一个
crypto/rand提供
Read(b[]byte)功能的包,因此我们可以用它通过一个调用获得所需的字节数。这在性能方面无济于事,因为它
crypto/rand实现了加密安全的伪随机数生成器,因此速度要慢得多。
因此,让我们坚持下去
math/rand。在
rand.Rand使用
rand.Source作为随机比特的源。
rand.Source是一个接口,它指定一种
Int63()int64方法:正是我们最新解决方案中需要和使用的唯一方法。
因此,我们实际上并不需要
rand.Rand(包的显式或全局共享
rand),
rand.Source对于我们来说,a 足够了:
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, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; { if remain == 0 { cache, remain = src.Int63(), letterIdxMax } if idx := int(cache & letterIdxMask); idx < len(letterBytes) { b[i] = letterBytes[idx] i-- } cache >>= letterIdxBits remain-- } return string(b)}另请注意,最后一种解决方案不需要您初始化(种子)未使用
Rand的
math/rand程序包的全局变量(并且
rand.Source已正确初始化/植入了种子)。
这里还要注意一件事:
math/rand状态包doc :
默认Source是安全的,可以供多个goroutine并发使用。
因此,默认来源比
Source可能获得的慢
rand.NewSource(),因为默认来源必须在并发访问/使用下提供安全性,而默认来源则不能提供安全性
rand.NewSource()(因此
Source,它返回的可能性更大)。
7.利用 strings.Builder
之前的所有解决方案都返回一个,
string其内容首先构建在切片中(
[]rune在 Genesis
中以及
[]byte后续的解决方案中),然后转换为
string。由于
string值是不可变的,因此最终的转换必须复制切片的内容,并且如果转换不能复制,则不能保证字符串的内容不会通过其原始切片进行修改。
Go 1.10推出
strings.Builder。
strings.Builder一种新的类型,我们可以用它来建立
string类似的内容
bytes.Buffer。它在内部使用进行操作
[]byte,完成后,我们可以
string使用其
Builder.String()方法获得最终值。但是,最酷的是它可以执行此操作而不执行我们上面刚刚谈到的复制操作。之所以敢于这样做,是因为未暴露用于构建字符串内容的字节片,因此可以确保没有人可以无意或恶意地修改它来更改生成的“不可变”字符串。
因此,我们的下一个想法是不要在切片中构建随机字符串,而是借助a
strings.Builder,因此一旦完成,我们就可以获取并返回结果,而无需对其进行复制。这可能会在速度方面有所帮助,并且绝对会在内存使用和分配方面有所帮助。
func RandStringBytesMaskImprSrcSB(n int) string { sb := strings.Builder{} sb.Grow(n) // A src.Int63() generates 63 random bits, enough for letterIdxMax characters! for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; { if remain == 0 { cache, remain = src.Int63(), letterIdxMax } if idx := int(cache & letterIdxMask); idx < len(letterBytes) { sb.WriteByte(letterBytes[idx]) i-- } cache >>= letterIdxBits remain-- } return sb.String()}请注意,在创建new之后
strings.Buidler,我们调用了它的
Builder.Grow()方法,确保它分配了足够大的内部切片(以避免在添加随机字母时重新分配)。
8.“模仿” strings.Builder
包装unsafe
strings.Builder``[]byte就像我们自己一样,在内部构建字符串。因此,基本上通过a进行操作会
strings.Builder产生一些开销,我们切换到的唯一
strings.Builder方法是避免切片的最终复制。
strings.Builder通过使用package避免最终副本
unsafe:
// String returns the accumulated string.func (b *Builder) String() string { return *(*string)(unsafe.Pointer(&b.buf))}问题是,我们也可以自己做。因此,这里的想法是切换回在中构建随机字符串
[]byte,但完成后,请勿将其转换
string为返回值,而是进行不安全的转换:获取一个
string指向我们的字节片的字符串数据。
这是可以做到的:
func RandStringBytesMaskImprSrcUnsafe(n int) string { b := make([]byte, n) // A src.Int63() generates 63 random bits, enough for letterIdxMax characters! for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; { if remain == 0 { cache, remain = src.Int63(), letterIdxMax } if idx := int(cache & letterIdxMask); idx < len(letterBytes) { b[i] = letterBytes[idx] i-- } cache >>= letterIdxBits remain-- } return *(*string)(unsafe.Pointer(&b))}(9.使用rand.Read()
)
Go
1.7添加了一个
rand.Read()函数和一个
Rand.Read()方法。为了获得更好的性能,我们应该尝试使用这些来一步读取所需的字节数。
这有一个小“问题”:我们需要多少个字节?我们可以说:与输出字母的数量一样多。我们认为这是一个较高的估计,因为字母索引使用的少于8位(1个字节)。但是在这一点上,我们已经变得越来越糟(因为获得随机位是“困难的部分”),而且我们得到的超出了需要。
还要注意,要保持所有字母索引的均等分布,可能会有一些我们将无法使用的“垃圾”随机数据,因此我们最终将跳过一些数据,因此在遍历所有数据时最终会变短字节片。我们将需要进一步“递归”获得更多随机字节。现在我们甚至失去了“单次
rand打包”的优势…
我们可以“某种程度上”优化从中获取的随机数据的使用
math.Rand()。我们可以估计需要多少字节(位)。1个字母需要
letterIdxBits位,而我们需要
n字母,因此我们需要将
n* letterIdxBits /8.0字节四舍五入。我们可以计算出随机索引不可用的可能性(请参见上文),因此我们可以请求更多的“可能”足够(如果事实并非如此,则重复该过程)。例如,我们可以将字节片处理为“位流”,为此,我们有一个不错的第三方lib
:(
github.com/icza/bitio公开:我是作者)。
但是基准代码仍然表明我们没有赢。为什么会这样呢?
最后一个问题的答案是因为
rand.Read()使用循环并不断调用,
Source.Int63()直到它填充了传递的切片为止。
RandStringBytesMaskImprSrc()解决方案的确切功能是,
没有
中间缓冲区,也没有增加复杂性。这就是为什么
RandStringBytesMaskImprSrc()继续保持王位。是的,
RandStringBytesMaskImprSrc()使用了不同步的
rand.Source不同于
rand.Read()。但是推理仍然适用。如果我们使用
Rand.Read()代替
rand.Read()(前者也是不同步的),则证明了这一点。
二。基准测试
好了,现在是对不同解决方案进行基准测试的时候了。
关键时刻:
BenchmarkRunes-4 2000000 723 ns/op 96 B/op 2 allocs/opBenchmarkBytes-4 3000000 550 ns/op 32 B/op 2 allocs/opBenchmarkBytesRmndr-4 3000000 438 ns/op 32 B/op 2 allocs/opBenchmarkBytesMask-4 3000000 534 ns/op 32 B/op 2 allocs/opBenchmarkBytesMaskImpr-4 10000000 176 ns/op 32 B/op 2 allocs/opBenchmarkBytesMaskImprSrc-4 10000000 139 ns/op 32 B/op 2 allocs/opBenchmarkBytesMaskImprSrcSB-4 10000000 134 ns/op 16 B/op 1 allocs/opBenchmarkBytesMaskImprSrcUnsafe-4 10000000 115 ns/op 16 B/op 1 allocs/op
只需从符文转换为字节,我们即可立即获得 24%的 性能提升,而内存需求则下降至 三分之一 。
摆脱
rand.Intn()并使用
rand.Int63()它可以再提高 20% 。
遮罩(在大索引的情况下重复)会稍微减慢(由于重复调用):- 22% …
但是,当我们利用63个随机位的全部(或大部分)(一次
rand.Int63()调用即可获得10个索引)时:可以节省大量时间: 3倍 。
如果我们使用(非默认值,新值)
rand.Source代替
rand.Rand,我们将再次获得 21%的 收益 。
如果我们使用
strings.Builder,我们获得了一个微小的 3.5% 的 速度 ,但我们也取得了 50%
的内存使用和分配的减少!真好!
最后,如果我们敢于使用package
unsafe而不是
strings.Builder,我们将再次获得 14%的 收益。
最后进行比较来对初始解:
RandStringBytesMaskImprSrcUnsafe()是 快6.3倍
比
RandStringRunes(),使用 六分之一 存储器和 半尽可能少的分配 。任务完成。



