|
我想要一个没有数字的随机字符串(大写或小写)Go中。实施此操作最快、最简单的方法是什么?
* o7 C7 Q9 q8 o6 E/ y2 l ~/ C+ ~8 z a6 O' b
解决方案:
* h% ?3 S$ W! V 问题要求最快最简单的方法。让我们解决最快的部分。我们将通过迭代获得最终和最快的代码。每次迭代的基准测试都可以在答案的最后找到。
* @% Q9 [/ ?4 ~; @+ s4 K, e所有的解决方案和基准测试代码都可以Go Playground上找到。Playground 上面的代码是一个测试文件,而不是一个可执行的文件。您必须将其保存在名称文件中XX_test.go并运行它
6 R: W! X0 P E0 ?; bgo test -bench . -benchmem
- \8 r9 ?( t9 B: ^& y2 e$ _, n 前言:' X+ W8 F7 @+ d8 B9 Y5 v
如果你只需要一个随机字符串,最快的解决方案不是首选。因此,保罗的解决方案是完美的。这就是性能是否重要。尽管前两步(Bytes和Remainder)这可能是一个可以接受的折衷方案:它们确实将性能提高了50%(见II. Benchmark部分中的确切数字),不会显著增加复杂性。' E& c# V* s+ d/ w1 J, n6 z
尽管如此,即使你不需要最快的解决方案,通读这个答案也可能具有冒险精神和教育意义。
1 ?4 B+ z' u3 S# ?一、改进1. Genesis (Runes)我们正在改进的原始通用解决方案是:8 Y! b0 o) n1 _/ I k
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 = letterRunes[rand.Intn(len(letterRunes))] } return string(b)}1 o5 q) c) ^/ m3 Y
2. 字节如果要选择和组合随机字符串的字符,只包括英文字母的大写和小写字母,我们只能使用字节,因为英文字母映射到 UTF-8 编码中的字节 1 至 1(包括 Go 存储字符串的方式)。
0 ] `" M _& }" \- o所以不是:( t, @1 [1 ~. q" `2 s" t- n
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
: C% j6 \/ R4 E0 c; E" p 我们可以用:" V2 V8 f, [2 ]6 `. D/ F' r* @
var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
6 T+ i8 c$ u, y' [7 e2 I( K7 b 或至更好:
, V( K( O! ]! F5 Uconst letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"+ w8 c( H1 D# D! Q0 T, ?
现在这已经是一个很大的改进:我们可以将它实现为 a const(有string常量但无切片常量)。作为额外的收获,表达式len(letters)也将是const! (len(s)如果s字符串常量,表达式为常量。. W) s8 N: R, x) _* m
什么代价?什么都没有。strings 可以索引,索引它的字节,完美,正是我们想要的。4 F3 \7 M- q- k$ |
下一个目的地是这样的:! P) `3 D$ T* ~: j& ]' c$ r- F% D
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)}' B* W% N! A. c3 c7 M8 D2 Q
3. 余数以前的解决方案通过调用rand.Intn()委托给什么?Rand.Intn()什么样的委托来指定随机字母?Rand.Int31n()。0 w- n9 t5 x# v; @2 s
与rand.Int与 63 个随机位的随机数相比,生成的随机数要慢得多。' R0 L3 c/ J" j1 F5 C
因此,我们可以简单地调用它rand.Int63()并使用除以后的余数len(letterBytes):
7 O1 Q' f* `9 }) k& m1 u. Vfunc RandStringBytesRmndr(n int) string b := make([]byte,n) for i := range b b = letterBytes[rand.Int63() % int64(len(letterBytes))] } return string(b)}
7 p" v; m* g1 ~# w$ W* V 缺点是所有字母的概率点是所有字母的概率不会完全相同(假设rand.Int63()产生所有 63 位数的概率相等)。虽然字母的数量远小于1
2 o* O: G D+ R$ G$ Z6 l假设你想要一个范围为 的随机数0,以使它更容易理解:..5。使用 3 个随机位,这将产生0..1比范围 2 倍概率数字2..5.数字范围为0,使用5个随机比特..6/32概率和数字范围的2..5和5/32的概率现在更接近预期。当达到 63 位时,增加位数会使这一点变得不那么重要。
( @/ _4 f& D; E4 _8 }$ m0 D4. Masking在以前的解决方案的基础上,我们可以使用与字母数量相同的最低数量来保持字母的均匀分布。因此,例如,如果我们有52个,它需要6个来表示它:52 = 110100b。因此,我们只使用 返回数字的最低 6 位rand.Int63()。为了保持字母的平均分布,我们只接受范围内的数字0..len(letterBytes)-1.如果最低水平较大,我们将丢弃它并查询新的随机数。3 f" K* N/ U* c
请注意,最低机会大于或等于len(letterBytes)通常小于0.5(0.25平均),这意味着即使在这种情况下,重复这种罕见的情况也会减少找不到好机会的数字。n重复后,我们仍然没有好的索引机会远远小于pow(0.5,n),这只是一个上面的估计。在 52 字母的情况下,只有 6 最低不好的机会(64-52)/64 = 0.19;这意味着例如, 10 重复后没有好数字的机会是1e-8.
3 j- b7 D. `2 n6 R B8 s+ ]所以这里是解决方案:$ C& i6 g% A5 ?
. O& C* A3 k! v5 H6 [7 w) |: z- const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"const letterIdxBits = bits to represent a letter index letterIdxMask = 15. Masking改进前面的解决方案只使用 返回的 63 个随机位中最低 6 位rand.Int63()。这是一种浪费,因为获取随机位是我们算法中最慢的部分。4 w1 q/ d3 @% y/ s; S4 m/ @) _( J3 U
- 如果我们有 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)}- Y, v0 g0 X# d; q5 }
6. 来源该改进的屏蔽挺好的,改进不了多少。我们可以,但不值得复杂。
" T5 m, n. X/ n: b$ U2 } m0 Y现在让我们找到其他需要改进的地方。随机来源。
3 X( }) m% t) V8 }/ @有一个crypto/rand提供一个包Read(b [\]byte)函数,所以我们可以用它来获得尽可能多的字节。这对性能没有帮助,因为crypto/rand实现加密安全伪随机数生成器,因此速度要慢得多。 }! U. z0 m5 n: Q
所以让我们坚持下去math/rand包装。在rand.Rand使用rand.Source作为随机比特的来源。rand.Source它指定了一个接口Int63() int64方法:这是我们在最新解决方案中需要和使用的唯一东西。
$ L/ {1 R8 G6 o所以我们真的不需要一个rand.Rand(显式或全局,共享rand包),arand.Source对我们来说已经足够了:
; R, a8 C' S# }# Q8 Yvar 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)}
, R* i/ s0 X9 ` 还需要注意的是,这个最终的解决方案并不要求你在全球初始化(种子)Rand的的math/rand包里没用(和我们一起)rand.Source正确的初始化/种子)。
4 ?6 a3 \/ W0 m9 f1 X- W这里还要注意一件事:math/rand状态包文件:0 C3 Z3 @7 T1 Y* z' r) I$ [
默认的 Source 可安全地被多个 goroutines 并发使用。
( m8 r- F# Q/ n5 _% D9 N( l因此,默认源比Source可能获得的a 慢rand.NewSource()因为默认源必须在并发访问/使用下提供安全,rand.NewSource()而不提供(因此Source返回更有可能更快)。
3 d- p; y) v& L( h/ T7.利用 strings.Builder所有以前的解决方案都返回 a ,string其内容首先构建在切片中([]rune在Genesis和[]byte在后续的解决方案中)string. 最终的转换必须复制切片的内容,因为string值是不可变的。如果转换不能复制,则不能保证字符串的内容不会通过其原始切片进行修改。详情请参阅[如何使用 utf8 字符串转换为 ]byte?和[golang: ]byte(string) vs []byte(*string)。
* ~) T/ `' d3 S) ~, u3 `0 t; jGo 1.10 引入strings.Builder。strings.Builder我们可以用它来构建一种新型号string类似于bytes.Buffer. 在内部使用 a[]byte构建内容,当我们完成时,我们可以string使用它的Builder.String()获得最终值的方法。但很酷的是,它可以完成这个操作,而无需执行我们刚才提到的复制。它之所以敢这样做,是因为用来构建字符串内容的字节片没有暴露,所以确保没有人能无意或恶意地修改它来改变产生的不可变字符串。! _0 U5 K; W, Z
所以我们的下一个想法不是在切片中构建随机字符串,而是在 a 的帮助下strings.Builder,因此,一旦我们完成,我们就可以在不复制的情况下获得并返回结果。这可能有助于速度,肯定有助于内存的使用和分配。( c8 ~) c, ]% c* |- c# E+ r0 D
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()}
- q% {7 g" r7 |4 A Q 请注意,注意new 之后strings.Buidler,我们调用了它的Builder.Grow()确保它分配足够大的内部切片(以避免在添加随机字母时重新分配)。
9 U& x& r0 {' {8.strings.Builder用包模仿unsafestrings.Builder在内部构建字符串,[]byte就像我们自己做的。所以基本上是通过 a 来做strings.Builder有一些开销,我们切换到的唯一一件事strings.Builder避免最终复制切片。
. p' p% G( e8 L7 g, Z: }$ Q6 ]/ t# \strings.Builder通过使用 package 避免最终副本unsafe:$ a w8 u2 v2 A0 _3 s( q9 ^
// String returns the accumulated string.func (b *Builder) String() string return *(*string)(unsafe.Pointer(&b.buf))}6 u6 U: Z8 f: r) s- ?2 Q: T k
问题是我们也可以自己做。所以这里的想法是切换回 a 构建随机字符串[]byte,但是,当我们完成它时,不要转换它string为 return,相反,进行不安全转换:获取string 指向我们的字节切片作为字符串数据a .7 Z% Z8 B# d; Z1 U* h; N9 d
这是怎么做到的:
0 {" l/ V4 p) S/ {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 >= letterIdxBits remain-- } return *(*string)(unsafe.Pointer(&b))}. [3 ~1 A2 {& t5 R0 ~" r
(9. 使用rand.Read())Go 1.7 加了一个rand.Read()函数和一个Rand.Read()方法。为了实现更好的性能,我们应该尝试使用这些来阅读我们需要尽可能多的字节。7 C1 L% _' v" X% m; q. G& l
有一个小问题:我们需要多少字节?我们可以说,字母的输出量与输出字母的数量相同。我们会认为这是一个上限估计,因为字母索引的使用量小于 8 (1 字节)。但在这一点上,我们做得更糟(因为获得随机位置是困难的部分),我们得到的超出了需求。
1 P" F5 k8 X/ w, L4 `' H3 a* }此外,请注意,为了保持所有字母索引的均匀分布,可能会有一些我们不能使用的垃圾随机数据,所以我们最终会跳过一些数据,所以当我们通过所有数据时,我们最终会有一个短字节切片。我们需要进一步交付才能获得更多的随机字节。现在我们甚至失去了单呼叫rand包装的优点......' U6 t8 E4 H4 w! E, k- p$ K4 O
我们可以稍微优化我们获得的随机数据的使用math.Rand()。我们可以估计需要多少字节(位)。1 字母需要letterIdxBits我们需要位置n所以我们需要字母n * letterIdxBits / 8.0字节四舍五入。我们可以计算随机索引不可用的概率(见上述),因此我们可以要求更多的更有可能(如果没有,我们将重复此过程)。例如,我们可以将字节切片视为位流,因此我们有一个很好的 3rd 方库:(github.com/icza/bitio披露:我是作者)。5 `5 n6 Q' d5 T) n7 a* n
但基准代码仍然表明我们没有获胜。为什么?' g. {" e: n i' l3 q8 U5 k+ c, `
最后一个问题的答案是因为rand.Read()使用循环并不断调用,Source.Int63()直到它填满传递的切片。RandStringBytesMaskImprSrc()解决方案所做的,没有中间缓冲区没有增加复杂性。这就是为什么RandStringBytesMaskImprSrc()留在宝座上。RandStringBytesMaskImprSrc()使用不同步的rand.Source不同rand.Read()Rand.Read()而不是证明这一点rand.Read()(前者也不同步)。
3 a( A3 ]! R' [- v R二、基准是时候基准测试不同的解决方案了。) x6 c5 o4 j0 S
关键时刻:
6 }$ Y! Y* r d- ?6 {4 YBenchmarkRunes- 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
: ]; c4 B% U @/ [ 我们立即从符文切换到字节24% 的性能提高,内存需求下降到三分之一。# ?# H# f1 J& g! l
摆脱rand.Intn()并使用rand.Int63()它会带来另一个20% 的提升。
: [. a' v+ g# p( y/ |- X7 S. k掩码(并在大索引的情况下重复)稍慢(因重复调用):- 22% …
8 Y! }( T8 ]) }2 ?' K }. ?, T6 Q然而,当我们使用全部(或大部分)63个随机位(来自一次)时rand.Int63()调用10 索引时:这将大大加快时间:3 倍。
) B: }, k; K" e3 ~假如我们使用(非默认,新的)rand.Source代替rand.Rand,我们再次获得21%。
3 J" {! L7 J: p+ U( C如果我们使用strings.Builder,我们得到了一个小的3.5%的速度,但是我们也得到了50%减少内存的使用和分配!那很好!
4 z$ ^1 G9 S" u' e; R最后,如果我们敢用 packageunsafe而不是strings.Builder,我们又得到了好的东西14%。0 T, j. {0 f2 M m" f( Y3 [5 b5 o$ Y. _
最后对初始解进行比较:RandStringBytesMaskImprSrcUnsafe()是快6.3倍比RandStringRunes(),使用六分之一存储器和尽量少分配半。任务完成。 |
|