快速產生一個隨機字串

來源:互聯網
上載者:User

目錄 [−]

  1. 最通用的方案
  2. 位元組替換rune
  3. 使用餘數
  4. 掩碼
  5. 掩碼加強版
  6. Source
  7. Benchmark代碼
  8. 其它提升

如何高效的產生一個隨機字串?這看似是一個簡單的問題,但是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/randrand.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
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.