如果想在Go語言中生成隻有隨機字符(大寫或小寫)、沒有數字的定長字符串。什麽方法最快最簡單?
這個問題要求“最快最簡單的方法”。我們將由淺入深,以循序漸進的方式給出最終最快的代碼。(可以在答案的最後找到每次迭代的基準測試。)
所有解決方案和基準測試代碼都可以在Go Playground上找到。 Playground上的代碼是測試文件,而不是可執行文件。將其保存到名為XX_test.go
的文件中,並使用go test -bench .
可以運行它。
一,逐步改進的方法
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(是如何存儲字符串)。
所以代替:
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
我們可以用:
var letters = []bytes("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
甚至更好的:
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
現在這已經是一個很大的改進:我們可以將它變為const
(有string常量
,但是沒有slice常量)。作為額外的收益,表達式len(letters)
也將是const
! (如果s
是一個字符串常量,則表達式len(s)
是常量。)
這樣的性能開銷是多少?幾乎沒有開銷! string
可以編入索引,索引其字節。完美,正是我們想要的。
所以我們的下一個方法如下:
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隨機bits的隨機數。
所以我們可以簡單地調用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
,而不是來自範圍2..5
。使用5個隨機位,0..1
範圍內的數字將與6/32
概率和2..5
範圍內的數字一起出現,其中5/32
概率現在更接近期望值。增加位數會使這一點變得不那麽重要,當達到63位時,它可以忽略不計。
4.掩碼
在前麵的解決方案的基礎上,我們可以通過使用隨機數的最低位來保證字母的平等分配。因此,例如,如果我們有52個字母,則需要6位來表示它:52 = 110100b
。所以我們隻使用rand.Int63()
返回的最低6位數。並且為了保持字母的平均分配,我們隻有”接受”落在0..len(letterBytes)-1
的範圍內的數字。如果最低位更大,我們將其丟棄並查詢新的隨機數。
請注意,最低位大於或等於len(letterBytes)
的可能性一般小於0.5
(平均為0.25
),這意味著即使出現這種情況,重複此”rare”案例也會降低找不到的好的數字的可能性。在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.掩碼改進
前麵的解決方案僅使用rand.Int63()
返回的63個隨機位中的最低6位。這是一種浪費,因為獲取隨機位是我們算法中最慢的部分。
如果我們有52個字母,那意味著6bits編碼字母索引。所以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
對我們來說已經足夠了:
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)
}
另請注意,最後一個解決方案不要求您初始化(播種)math/rand
軟件包的全局Rand
,因為它未被使用(並且我們的rand.Source
已正確初始化/設置種子)。
還有一點需要注意:math/rand
的包文檔說明:
The default Source is safe for concurrent use by multiple goroutines.
所以默認源比rand.NewSource()
可能獲得的Source
慢,因為默認源必須在並發訪問/使用時提供安全性,而rand.NewSource()
不提供此功能(因此它返回的Source可以
更快) )。
二、評測
好吧,讓我們對不同的解決方案進行基準測試。
BenchmarkRunes 1000000 1703 ns/op
BenchmarkBytes 1000000 1328 ns/op
BenchmarkBytesRmndr 1000000 1012 ns/op
BenchmarkBytesMask 1000000 1214 ns/op
BenchmarkBytesMaskImpr 5000000 395 ns/op
BenchmarkBytesMaskImprSrc 5000000 303 ns/op
隻需從字符串切換到字節,我們立即獲得22%的性能提升。
擺脫rand.Intn()
並使用rand.Int63()
代替另外24%的提升。
掩碼(並且在大索引的情況下重複)減慢一點(由於重複調用):-20%……
但是當我們使用63個隨機位中的所有(或大多數)(來自一個rand.Int63()
調用的10個索引)時:它加速了3.4倍。
最後,如果我們解決了(non-default,新)rand.Source
代替rand.Rand
,我們再次獲得23%。
比較最終解決方案:RandStringBytesMaskImprSrc()
比RandStringRunes()
快5.6倍。
參考資料