golang で regexp パッケージを使うときに気をつけること
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() }
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..`) } } }