目錄 [−]
- 最通用的方案
- 位元組替換rune
- 使用餘數
- 掩碼
- 掩碼加強版
- Source
- Benchmark代碼
- 其它提升
如何高效的產生一個隨機字串?這看似是一個簡單的問題,但是icza卻通過例子,逐步最佳化,實現了一個更高效的隨機字串的演算法。這是來自的來自stackoverflow上的一個問題:How to generate a random string of a fixed length in Go?, 大家群策群力,提出了很好的方案和反饋,尤其是icza的回答。 本文翻譯和整理自這條問答。
問題是這樣的:
我想要一個Go實現的固定長度的隨機字串(包括大小寫字母,但是沒有數字),哪種方式最快最簡單?
最佳化基於Paul Hankin提出的一種方案(第一種方案),也就是最基本最容易理解的一種方案, icza基於這個方案逐步最佳化。
最通用的方案
最普通方案就是隨機產生每個字元,所以整體字串也是隨機的。這樣的好處是可以控制要使用的字元。
12345678910111213 |
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)} |
位元組替換rune
如果需求是只使用英語字母字元(包括大小寫),那麼我們可以使用byte替換rune,因為UTF-8編碼中英語字母和byte是一一對應的。
123456789 |
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)} |
使用餘數
上一步中我們使用rand.Intn
來隨機播放一個字元, rand.Intn
會調用Rand.Intn
, 而Rand.Intn
會調用Rand.Int31n
,它會比直接調用rand.Int63
慢,後者會產生一個63bit的隨機整數。
我們可以使用rand.Int63
,然後除以len(letterBytes)
的餘數來選擇字元:
1234567 |
func RandStringBytesRmndr(n int) string { b := make([]byte, n) for i := range b { b[i] = letterBytes[rand.Int63() % int64(len(letterBytes))] } return string(b)} |
這個實現明顯會比上面的解決方案快,但是有一點小小的瑕疵:那就是字元被選擇的機率並不是完全一樣。但是這個差別是非常非常的小(字元的數量52遠遠小於1<<63 -1),
只是理論上會有差別,實踐中可以忽略不計。
掩碼
通過前面的方案,我們可以看到我們並不需要太多的bit來決定字元的平均分布,事實上我們只需要隨機整數的後幾個bit就可以來選擇字母。對於52個英語字母(大小寫), 只需要6個bit就可以實現均勻分布(52=110100b
),所以我們可以使用rand.Int63
後6個bit來實現,我們只接受後六位在0..len(letterBytes)-1
的隨機數,如果不在這個範圍,丟棄重選。 通過掩碼就可以得到一個整數的後6個bit。
12345678910111213141516 |
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)} |
掩碼加強版
上面有個不好的地方,會產生大量的丟棄的case,造成重選和浪費。rand.Int63
會產生63bit的隨機數,如果我們把它分成6份,那麼一次就可以產生10個6bit的隨機數。這樣就減少了浪費。
12345678910111213141516171819202122232425 |
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)} |
Source
上面的代碼的確好,沒有太多可以改進的地方,即使可以提升,也得花費很大的複雜度。
我們可以從另外一個方面進行最佳化,那就是提高隨機數的產生(source)。
crypto/rand
包提供了Read(b []byte)
的方法,它可以隨機產生我們所需bit的位元組,但是因為處於安全方面的設計和檢查,它的隨機數產生比較慢。
我們再轉回math/rand
,rand.Rand
使用rand.Source
來產生隨機bit。rand.Source
是一個介面,提供了Int63() int64
,正是我們所需要的。
所以我們可以直接使用rand.Source
,而不是全域或者共用的隨機源。
1234567891011121314151617181920 |
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.Source
更好。
下面的代碼是全域的隨機源,可以看到Lock/Unlock
的使用。
12345678910111213141516171819202122 |
func Int63() int64 { return globalRand.Int63() }var globalRand = New(&lockedSource{src: NewSource(1).(Source64)})type lockedSource struct {lk sync.Mutexsrc Source64}func (r *lockedSource) Int63() (n int64) {r.lk.Lock()n = r.src.Int63()r.lk.Unlock()return} |
Go1.7中增加了rand.Read()
方法和Rand.Read()
函數,我們可以嘗試使用它得到一組隨機bit,用來擷取更高的效能。
一個小問題就是取多少位元組的隨機數比較好?我們可以說: 和輸出字元一樣多的。這是一個上限估計,因為字元的索引會少於8bit。
為了維護字元的均勻分布,我們不得不丟棄一些隨機數,這可能會擷取更多的隨機數,所以只能預估大約需要n * letterIdxBits / 8.0
位元組的隨機byte。
當然最好的驗證方法就是寫一個Benchmark,附錄是benchmark的代碼,以下是測試的結果:
123456 |
BenchmarkRunes 1000000 1703 ns/opBenchmarkBytes 1000000 1328 ns/opBenchmarkBytesRmndr 1000000 1012 ns/opBenchmarkBytesMask 1000000 1214 ns/opBenchmarkBytesMaskImpr 5000000 395 ns/opBenchmarkBytesMaskImprSrc 5000000 303 ns/op |
Benchmark代碼
BenchmarkRandomString_test.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
package mainimport ("math/rand""testing""time")// Implementationsfunc 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)}const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"const (letterIdxBits = 6 // 6 bits to represent a letter indexletterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBitsletterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits)func RandStringBytes(n int) string {b := make([]byte, n)for i := range b {b[i] = letterBytes[rand.Intn(len(letterBytes))]}return string(b)}func RandStringBytesRmndr(n int) string {b := make([]byte, n)for i := range b {b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]}return string(b)}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)}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 >>= letterIdxBitsremain--}return string(b)}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 >>= letterIdxBitsremain--}return string(b)}// Benchmark functionsconst n = 16func BenchmarkRunes(b *testing.B) {for i := 0; i < b.N; i++ {RandStringRunes(n)}}func BenchmarkBytes(b *testing.B) {for i := 0; i < b.N; i++ {RandStringBytes(n)}}func BenchmarkBytesRmndr(b *testing.B) {for i := 0; i < b.N; i++ {RandStringBytesRmndr(n)}}func BenchmarkBytesMask(b *testing.B) {for i := 0; i < b.N; i++ {RandStringBytesMask(n)}}func BenchmarkBytesMaskImpr(b *testing.B) {for i := 0; i < b.N; i++ {RandStringBytesMaskImpr(n)}}func BenchmarkBytesMaskImprSrc(b *testing.B) {for i := 0; i < b.N; i++ {RandStringBytesMaskImprSrc(n)}} |
其它提升
其實如果能替換一個效能更好的隨機數產生演算法,可能效能會更好,我使用Xorshift演算法實現了一個快速的隨機數產生器, 和前面的實現做了比較,發覺效能會更好一點。
1234567 |
BenchmarkRunes-4 1000000 1396 ns/opBenchmarkBytes-4 2000000 799 ns/opBenchmarkBytesRmndr-4 3000000 627 ns/opBenchmarkBytesMask-4 2000000 719 ns/opBenchmarkBytesMaskImpr-4 10000000 260 ns/opBenchmarkBytesMaskImprSrc-4 10000000 227 ns/opBenchmarkBytesMaskImprXorshiftSrc-4 10000000 205 ns/op |