Go言語でランダムな文字列を作ってみる
Go言語で、ランダムな文字列を生成する方法について考えてみます。
この記事では math/rand
ではなく crypto/rand
を使います。
※記事中で最終的に実装したものは https://github.com/najeira/randstr パッケージ化しましたので、ただ使いたい場合はそちらで。
Phase 1
まず単純に考えて、文字種の範囲内で乱数を生成して、文字列からピックアップしていきます。
crypto/rand
だと [0, max) の乱数を生成してくれる Int
関数に max として big.Int
を渡します。
package randstr
import (
"crypto/rand"
"math/big"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
func Phase1(n int) string {
buf := make([]byte, n)
max := new(big.Int)
max.SetInt64(int64(len(letterBytes)))
for i := range buf {
r, err := rand.Int(rand.Reader, max)
if err != nil {
panic(err)
}
buf[i] = letterBytes[r.Int64()]
}
return string(buf)
}
func BenchmarkPhase1(b *testing.B) {
for i := 0; i < b.N; i++ {
Phase1(100)
}
}
BenchmarkPhase1-4 50000 29519 ns/op 8372 B/op 303 allocs/op
※Phase 2 以降もベンチマークのコードは同等
Int
関数内で new(big.Int)
が実行されるので、パフォーマンスはあまりよくありません。
new(big.Int) …… ISUCON 7 で見たな……
Phase 2
文字はアルファベット大小と数字で62文字あります。62文字ということは6ビットの範囲内です。なので、1バイト(8ビット)の乱数を取得して、6ビット分だけ使えばよさそうです。
1バイトの乱数を取得し、6ビットでマスクします。この最大値が63で、62と63が文字種の数を超えるので、その場合は無視してやり直し。61以下なら文字をピックアップします。
package randstr
import (
"crypto/rand"
"math/big"
)
const (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
letterIdxMask = 0x3F // 63 0b111111
)
func Phase2(n int) string {
src := make([]byte, 1)
buf := make([]byte, n)
for i := 0; i < n; {
if _, err := rand.Read(src); err != nil {
panic(err)
}
idx := int(src[0] & letterIdxMask)
if idx < len(letterBytes) {
buf[i] = letterBytes[idx]
i++
}
}
return string(buf)
}
BenchmarkPhase2-4 100000 19922 ns/op 225 B/op 3 allocs/op
だいぶ速くなりました。
Phase 3
1バイトずつ乱数を読まずに、いっきに数バイト読み込めばいいのではないか。
package randstr
import (
"crypto/rand"
"math/big"
)
const (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
letterIdxMask = 0x3F // 63 0b111111
)
func Phase3(n int) string {
src := make([]byte, n)
buf := make([]byte, n)
for i, j := 0, 0; i < n; j++ {
pos := j % n
if pos == 0 {
if _, err := rand.Read(src); err != nil {
panic(err)
}
}
idx := int(src[pos] & letterIdxMask)
if idx < len(letterBytes) {
buf[i] = letterBytes[idx]
i++
}
}
return string(buf)
}
BenchmarkPhase3-4 100000 18506 ns/op 336 B/op 3 allocs/op
さほど速くならず、メモリ消費がアップ。
ただ、生成する文字列の長さが短い(例えば10)のときは、Phase3はPhase2より速いです。10文字でのベンチマーク:
BenchmarkPhase2-4 1000000 1625 ns/op 32 B/op 3 allocs/op
BenchmarkPhase3-4 1000000 1222 ns/op 48 B/op 3 allocs/op
Phase 4
結果用のバッファと、乱数用のバッファで、ふたつ確保しているけれど、1つでいいのではないか。乱数をバッファに読み込んで、文字に置き換えていく方式。
package randstr
import (
"crypto/rand"
"math/big"
)
const (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
letterIdxMask = 0x3F // 63 0b111111
)
func Phase4(n int) string {
buf := make([]byte, n)
if _, err := rand.Read(buf); err != nil {
panic(err)
}
for i := 0; i < n; {
idx := int(buf[i] & letterIdxMask)
if idx < len(letterBytes) {
buf[i] = letterBytes[idx]
i++
} else {
if _, err := cryptorand.Read(buf[i:i+1]); err != nil {
panic(err)
}
}
}
return string(buf)
}
BenchmarkPhase4-4 200000 9144 ns/op 224 B/op 2 allocs/op
速くなった。
まとめ:
BenchmarkPhase1-4 50000 29519 ns/op 8372 B/op 303 allocs/op
BenchmarkPhase2-4 100000 19922 ns/op 225 B/op 3 allocs/op
BenchmarkPhase3-4 100000 18506 ns/op 336 B/op 3 allocs/op
BenchmarkPhase4-4 200000 9144 ns/op 224 B/op 2 allocs/op
なお、pprof で Phase4 を調べてみると crypto.Read
から呼ばれる syscall.Syscall
が支配的。
どなたか、もうちょっと速くできたりしますでしょうか?
また、乱数的にまずい処理があれば教えてください!
License: MIT
なお math/rand
の場合では https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang が参考になります。
この記事はQiitaの記事をエクスポートしたものです
Discussion