at kaneshin

Free space for me.

golang で regexp パッケージを使うときに気をつけること

f:id:laplus-knsn:20161015132839p:plain


golang の Regexp は初期化の Compile コストがそこそこありますが、実は正規表現のパターンと対象となる文字列によって、初期化コストが無視できる(パターンと文字列に支配される)くらい遅くなります。よく言われる regexp の初期化コスト問題が無視できると言われても、正直、全く嬉しくないですね。

ただ、コーディング中に regexp パッケージを使わなければいけない場面は出てくるため、なるべくコストの掛からない実装を心がけています。

Compile/MustCompile

初期化コストはなくすため、グローバルに保持するようにします。

定義

var re = regexp.MustCompile("[a-z]{3}")

func main() {
    fmt.Println(re.FindAllString("foobarbazqux", -1))
    // => [foo bar baz qux]
}

グローバルで定義しておけば起動時に実行されるので、Compile でエラーチェックではなく、MustCompile を使用してそもそも起動できなくしています。

var re = regexp.MustCompile("([a-z]{3}")
// => panic: regexp: Compile(`([a-z]{3}`): error parsing regexp: missing closing ): `([a-z]{3}`

func main() {
    fmt.Println(re.FindAllString("foobarbazqux", -1))
}

関数実行で隠蔽してしまうのもアリ。

var finder = func() func(string) []string {
    re := regexp.MustCompile("[a-z]{3}")

    return func(str string) []string {
        return re.FindAllString(str, -1)
    }
}()

func main() {
    fmt.Println(finder("foobarbazqux"))
    // => [foo bar baz qux]
}

Benchmark

regexp を初期化する/しないのベンチマークです。ベンチデータはそこそこ大きいデータを使用しています。

// BenchmarkRegexpMustCompile1-16
// 5000            385760 ns/op          100850 B/op        830 allocs/op
func BenchmarkRegexpMustCompile1(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        re := regexp.MustCompile("[a-z]{3}")
        _ = re.FindAllString(`Lorem ipsum dolor sit amet, consectetur..`)
    }
}

// BenchmarkRegexpMustCompile2-16
// 5000            367423 ns/op           59961 B/op        805 allocs/op
func BenchmarkRegexpMustCompile2(b *testing.B) {
    re := regexp.MustCompile("[a-z]{3}")
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = re.FindAllString(`Lorem ipsum dolor sit amet, consectetur..`)
    }
}

func (*Regexp) Copy *Regexp

go1.6 から用意された Copy 関数です。

func (re *Regexp) Copy() *Regexp

コピーの利点は下記の通りです。

Copy returns a new Regexp object copied from re. When using a Regexp in multiple goroutines, giving each goroutine its own copy helps to avoid lock contention.

regexp - The Go Programming Language

要は、 Regexp が複数の goroutine で使われても大丈夫なように goroutine safe に作られているのですが、競合されないようにロックをしているので複数の goroutine で使いまわされることのある Regexp は使用前に Copy せよ、ということになります。

上にあったコードから参考に修正すると、Regexp の関数を実行する前に re := re.Copy() でコピーを取得しています。

var finder = func() func(string) []string {
    re := regexp.MustCompile("[a-z]{3}")

    return func(str string) []string {

        // Copy regexp.
        re := re.Copy()
        return re.FindAllString(str, -1)
    }
}()

func main() {
    list := []string{
        "foo bar baz",
        "kaneshin",
        "shintaro kaneko",
    }

    var wg sync.WaitGroup
    for _, str := range list {
        wg.Add(1)
        str := str
        go func() {
            defer wg.Done()
            fmt.Println(finder(str))
        }()
    }
    wg.Wait()
}

The Go Playground

Benchmark

// !! No Copy
// BenchmarkParallelRegexpMustCompileNoCopy-16
// 5000            375899 ns/op           99689 B/op        817 allocs/op

// BenchmarkParallelRegexpMustCompileCopy-16
// 30000             56602 ns/op           59976 B/op        805 allocs/op
func BenchmarkParallelRegexpMustCompileCopy(b *testing.B) {

    re := regexp.MustCompile("[a-z]{3}")

    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {

        // Copy Regexp
        // https://golang.org/src/regexp/all_test.go?s=20028:20077#L718
        re := re.Copy() // ❤️
        for pb.Next() {
            _ = re.FindAllString(`Lorem ipsum dolor sit amet, consectetur..`)
        }
    }
}