|
我想要一个没有数字的随机字符串(大写或小写)Go中。实施此操作最快、最简单的方法是什么?& s# O) Y H% v
; v+ [) v! \: W/ G' n7 c 解决方案: $ z$ \" B8 C; i+ V3 A$ N3 ?1 I
问题要求最快最简单的方法。让我们解决最快的部分。我们将通过迭代获得最终和最快的代码。每次迭代的基准测试都可以在答案的最后找到。" h, `! o S2 y
所有的解决方案和基准测试代码都可以Go Playground上找到。Playground 上面的代码是一个测试文件,而不是一个可执行的文件。您必须将其保存在名称文件中XX_test.go并运行它' Q0 f0 G( i% @3 i
go test -bench . -benchmem' U7 }- K4 V3 y5 ]( l1 q% s \' w
前言:
. {" |/ A# L9 c1 @8 N如果你只需要一个随机字符串,最快的解决方案不是首选。因此,保罗的解决方案是完美的。这就是性能是否重要。尽管前两步(Bytes和Remainder)这可能是一个可以接受的折衷方案:它们确实将性能提高了50%(见II. Benchmark部分中的确切数字),不会显著增加复杂性。
d1 H, j. U$ b, e! n" e尽管如此,即使你不需要最快的解决方案,通读这个答案也可能具有冒险精神和教育意义。
- E. Y* J! m, y' ~* D7 r: _一、改进1. Genesis (Runes)我们正在改进的原始通用解决方案是:
2 B1 O# u8 Y4 Nfunc init() rand.Seed(time.Now().UnixNano())}var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")func RandStringRunes(n int) string b := make([]rune,n) for i := range b b = letterRunes[rand.Intn(len(letterRunes))] } return string(b)}1 O( r; E4 t/ W
2. 字节如果要选择和组合随机字符串的字符,只包括英文字母的大写和小写字母,我们只能使用字节,因为英文字母映射到 UTF-8 编码中的字节 1 至 1(包括 Go 存储字符串的方式)。% ?( J0 W& ~7 R. c
所以不是:: U6 G. Z5 q$ ]3 @& Q+ o: H
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
4 d" f! K6 e b 我们可以用:
$ z S2 B( v. P4 ^) n3 P5 |var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
0 z! j( S+ l2 y. T 或至更好:
4 v, a7 r* x7 l5 f" Tconst letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"- L2 k! g) c" `" H3 @- \# {
现在这已经是一个很大的改进:我们可以将它实现为 a const(有string常量但无切片常量)。作为额外的收获,表达式len(letters)也将是const! (len(s)如果s字符串常量,表达式为常量。
! a4 C3 j" m( b" D' {- \$ g1 l什么代价?什么都没有。strings 可以索引,索引它的字节,完美,正是我们想要的。
" K/ Q- ]2 i+ y; @% n0 T下一个目的地是这样的:* @2 }* L- O/ h. W& u- y
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"func RandStringBytes(n int) string b := make([]byte,n) for i := range b b = letterBytes[rand.Intn(len(letterBytes))] } return string(b)}" _. a+ ] b0 S" r
3. 余数以前的解决方案通过调用rand.Intn()委托给什么?Rand.Intn()什么样的委托来指定随机字母?Rand.Int31n()。
$ o) Z! i9 C* w# i1 U/ g4 Q8 K* x与rand.Int与 63 个随机位的随机数相比,生成的随机数要慢得多。/ |1 H& P* H0 G- J3 m. ?$ W" C4 C$ ^
因此,我们可以简单地调用它rand.Int63()并使用除以后的余数len(letterBytes):
; |& Y% q3 R; l4 Rfunc RandStringBytesRmndr(n int) string b := make([]byte,n) for i := range b b = letterBytes[rand.Int63() % int64(len(letterBytes))] } return string(b)}
, N8 ^- ^, y5 x2 [5 j 缺点是所有字母的概率点是所有字母的概率不会完全相同(假设rand.Int63()产生所有 63 位数的概率相等)。虽然字母的数量远小于1
9 ~" T) F/ t( c# @9 \假设你想要一个范围为 的随机数0,以使它更容易理解:..5。使用 3 个随机位,这将产生0..1比范围 2 倍概率数字2..5.数字范围为0,使用5个随机比特..6/32概率和数字范围的2..5和5/32的概率现在更接近预期。当达到 63 位时,增加位数会使这一点变得不那么重要。: a% ]& A' j3 d- S
4. Masking在以前的解决方案的基础上,我们可以使用与字母数量相同的最低数量来保持字母的均匀分布。因此,例如,如果我们有52个,它需要6个来表示它:52 = 110100b。因此,我们只使用 返回数字的最低 6 位rand.Int63()。为了保持字母的平均分布,我们只接受范围内的数字0..len(letterBytes)-1.如果最低水平较大,我们将丢弃它并查询新的随机数。
. C' j* Z4 j: m! g- r请注意,最低机会大于或等于len(letterBytes)通常小于0.5(0.25平均),这意味着即使在这种情况下,重复这种罕见的情况也会减少找不到好机会的数字。n重复后,我们仍然没有好的索引机会远远小于pow(0.5,n),这只是一个上面的估计。在 52 字母的情况下,只有 6 最低不好的机会(64-52)/64 = 0.19;这意味着例如, 10 重复后没有好数字的机会是1e-8.% X0 @/ t$ I, A* X& \6 w
所以这里是解决方案:
5 o2 J* E7 s3 J1 V* P% p# x. D) e7 w% D4 J; B
- const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"const letterIdxBits = bits to represent a letter index letterIdxMask = 15. Masking改进前面的解决方案只使用 返回的 63 个随机位中最低 6 位rand.Int63()。这是一种浪费,因为获取随机位是我们算法中最慢的部分。
$ ?. k6 F# w- h# p - 如果我们有 52 字母,这意味着 6 编码一个字母索引。因此, 63 随机位可指定63/6 = 10不同的字母索引。让我们使用所有这些 10 :[code]const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"const letterIdxBits = bits to represent a letter index letterIdxMask = 1= if remain == cache,remain = rand.Int63(),letterIdxMax if idx := int(cache & letterIdxMask); idx >= letterIdxBits remain-- } return string(b)}- p+ D. O+ R9 i
6. 来源该改进的屏蔽挺好的,改进不了多少。我们可以,但不值得复杂。' U6 r! Q4 L! m
现在让我们找到其他需要改进的地方。随机来源。( `* b/ D# g' i. _
有一个crypto/rand提供一个包Read(b [\]byte)函数,所以我们可以用它来获得尽可能多的字节。这对性能没有帮助,因为crypto/rand实现加密安全伪随机数生成器,因此速度要慢得多。: i# u( w7 Z! N ~; S; g; V
所以让我们坚持下去math/rand包装。在rand.Rand使用rand.Source作为随机比特的来源。rand.Source它指定了一个接口Int63() int64方法:这是我们在最新解决方案中需要和使用的唯一东西。
- g$ X( h! h) o4 f3 @5 [* A所以我们真的不需要一个rand.Rand(显式或全局,共享rand包),arand.Source对我们来说已经足够了:, D9 |$ U! N l! @( 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 >= letterIdxBits remain-- } return string(b)}& V8 X% e& e9 z$ ~4 T2 b, L
还需要注意的是,这个最终的解决方案并不要求你在全球初始化(种子)Rand的的math/rand包里没用(和我们一起)rand.Source正确的初始化/种子)。* b. t! m0 g' x7 u
这里还要注意一件事:math/rand状态包文件:
& H$ C; S* r1 ~: b2 J+ W& T; J# p默认的 Source 可安全地被多个 goroutines 并发使用。9 D4 G. s, f- b' Y* @) f
因此,默认源比Source可能获得的a 慢rand.NewSource()因为默认源必须在并发访问/使用下提供安全,rand.NewSource()而不提供(因此Source返回更有可能更快)。
" @: M' k# v! e" X( S. r! i" C. Y7.利用 strings.Builder所有以前的解决方案都返回 a ,string其内容首先构建在切片中([]rune在Genesis和[]byte在后续的解决方案中)string. 最终的转换必须复制切片的内容,因为string值是不可变的。如果转换不能复制,则不能保证字符串的内容不会通过其原始切片进行修改。详情请参阅[如何使用 utf8 字符串转换为 ]byte?和[golang: ]byte(string) vs []byte(*string)。& q8 q8 C& u. {* y! u& }* T
Go 1.10 引入strings.Builder。strings.Builder我们可以用它来构建一种新型号string类似于bytes.Buffer. 在内部使用 a[]byte构建内容,当我们完成时,我们可以string使用它的Builder.String()获得最终值的方法。但很酷的是,它可以完成这个操作,而无需执行我们刚才提到的复制。它之所以敢这样做,是因为用来构建字符串内容的字节片没有暴露,所以确保没有人能无意或恶意地修改它来改变产生的不可变字符串。9 p! f) j$ |. j/ n2 @
所以我们的下一个想法不是在切片中构建随机字符串,而是在 a 的帮助下strings.Builder,因此,一旦我们完成,我们就可以在不复制的情况下获得并返回结果。这可能有助于速度,肯定有助于内存的使用和分配。* E% a! f @- |/ U) y+ i5 s; J2 g
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 >= letterIdxBits remain-- } return sb.String()}5 Z2 ^9 |% H& t3 }5 B; f* \# M
请注意,注意new 之后strings.Buidler,我们调用了它的Builder.Grow()确保它分配足够大的内部切片(以避免在添加随机字母时重新分配)。3 M+ u2 F1 @8 W0 i2 q
8.strings.Builder用包模仿unsafestrings.Builder在内部构建字符串,[]byte就像我们自己做的。所以基本上是通过 a 来做strings.Builder有一些开销,我们切换到的唯一一件事strings.Builder避免最终复制切片。6 m& T3 w! l- s$ ]
strings.Builder通过使用 package 避免最终副本unsafe:
" ~/ [+ T5 p8 T9 r8 i U# b// String returns the accumulated string.func (b *Builder) String() string return *(*string)(unsafe.Pointer(&b.buf))}
' \9 L0 \7 O9 A" g8 L2 K 问题是我们也可以自己做。所以这里的想法是切换回 a 构建随机字符串[]byte,但是,当我们完成它时,不要转换它string为 return,相反,进行不安全转换:获取string 指向我们的字节切片作为字符串数据a .
* @7 u/ D2 K( m4 l M+ N这是怎么做到的:
4 C4 D! E1 M' qfunc 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 >= letterIdxBits remain-- } return *(*string)(unsafe.Pointer(&b))}9 f9 z2 s, F8 h6 g
(9. 使用rand.Read())Go 1.7 加了一个rand.Read()函数和一个Rand.Read()方法。为了实现更好的性能,我们应该尝试使用这些来阅读我们需要尽可能多的字节。7 S% J! g; X) `$ ^/ ^. b, m
有一个小问题:我们需要多少字节?我们可以说,字母的输出量与输出字母的数量相同。我们会认为这是一个上限估计,因为字母索引的使用量小于 8 (1 字节)。但在这一点上,我们做得更糟(因为获得随机位置是困难的部分),我们得到的超出了需求。( t4 I" x) e; R/ e
此外,请注意,为了保持所有字母索引的均匀分布,可能会有一些我们不能使用的垃圾随机数据,所以我们最终会跳过一些数据,所以当我们通过所有数据时,我们最终会有一个短字节切片。我们需要进一步交付才能获得更多的随机字节。现在我们甚至失去了单呼叫rand包装的优点...... R% m# U3 T* O* o% ~" Z
我们可以稍微优化我们获得的随机数据的使用math.Rand()。我们可以估计需要多少字节(位)。1 字母需要letterIdxBits我们需要位置n所以我们需要字母n * letterIdxBits / 8.0字节四舍五入。我们可以计算随机索引不可用的概率(见上述),因此我们可以要求更多的更有可能(如果没有,我们将重复此过程)。例如,我们可以将字节切片视为位流,因此我们有一个很好的 3rd 方库:(github.com/icza/bitio披露:我是作者)。
8 c [3 c$ ~- r7 W, u6 K但基准代码仍然表明我们没有获胜。为什么?
. R# F0 l) K6 P3 b4 O最后一个问题的答案是因为rand.Read()使用循环并不断调用,Source.Int63()直到它填满传递的切片。RandStringBytesMaskImprSrc()解决方案所做的,没有中间缓冲区没有增加复杂性。这就是为什么RandStringBytesMaskImprSrc()留在宝座上。RandStringBytesMaskImprSrc()使用不同步的rand.Source不同rand.Read()Rand.Read()而不是证明这一点rand.Read()(前者也不同步)。- o. N9 y) { c4 l
二、基准是时候基准测试不同的解决方案了。
9 q$ H$ Z" F! y4 Q- o- Y关键时刻:6 \0 _! D8 E6 ~( `
BenchmarkRunes- ns/op 96 B/op 2 allocs/opBenchmarkBytes- 5500 ns/op 32 B/op 2 allocs/opBenchmarkBytesRmndr- 3万0000000000000000000000000300000000000000000000000000000000000000000000000000000ns/op 32 B/op 2 allocs/opBenchmarkBytesMask- 3万00000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ns/op 32 B/op 2 allocs/opBenchmarkBytesMaskImpr- ns/op 32 B/op 2 allocs/opBenchmarkBytesMaskImprSrc- ns/op 32 B/op 2 allocs/opBenchmarkBytesMaskImprSrcSB- 1000000000000000000000000ns/op 16 B/op 1 allocs/opBenchmarkBytesMaskImprSrcUnsafe- 115 ns/op 16 B/op 1 allocs/op
4 {0 \# f" Y: J1 U8 v d2 o 我们立即从符文切换到字节24% 的性能提高,内存需求下降到三分之一。
5 c. n6 X/ k* }. Q: R# v! r摆脱rand.Intn()并使用rand.Int63()它会带来另一个20% 的提升。- T/ P$ q3 E3 R' ]# O2 d
掩码(并在大索引的情况下重复)稍慢(因重复调用):- 22% …
( _- x1 v6 V' I( o( i# f2 J5 t2 F! k- k然而,当我们使用全部(或大部分)63个随机位(来自一次)时rand.Int63()调用10 索引时:这将大大加快时间:3 倍。
' H0 Q9 U: T0 _% {/ X7 l假如我们使用(非默认,新的)rand.Source代替rand.Rand,我们再次获得21%。
L/ H, j. h( @9 L1 y如果我们使用strings.Builder,我们得到了一个小的3.5%的速度,但是我们也得到了50%减少内存的使用和分配!那很好!+ B# O6 i! u: Z" t3 m( r
最后,如果我们敢用 packageunsafe而不是strings.Builder,我们又得到了好的东西14%。
0 I+ ?1 s% }* J( Y7 [) A最后对初始解进行比较:RandStringBytesMaskImprSrcUnsafe()是快6.3倍比RandStringRunes(),使用六分之一存储器和尽量少分配半。任务完成。 |
|