LoginSignup
5
1

More than 5 years have passed since last update.

Go言語でランダムな文字列を作ってみる

Last updated at Posted at 2017-12-28

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 が参考になります。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1