Edited at
DiverseDay 10

go の regexp が遅いと知らずに regexp で form に fillinするモジュールを書いた

More than 3 years have passed since last update.

go で、html fillinform をしたかったので、@gfx さんの HTML::FillInForm::Lite を go に移植しました。こちら→ fillinform

同様の機能をもつパッケージには、htmlfiller がありました。こちらは regexp ではなく exp-html をつかっています。

まずは愚直に regexp の実装をそのまま移植してみたんですが、なにしろ go の regexp が遅かったので、調べてみると go の regexp が遅いのは 天地開闢から周知の事実でした。

http://qiita.com/naoina/items/d71ddfab31f4b29f6693

今もあまり変わってないようです。

ということで、このままだと perl の高速な regexp には到底太刀打ち出来ないので regexp を使わないようにできそうなところとか、そもそも自分の残念な go 実装を書き換えました。


unquote を bytes.Trim に

func (f Filler) unquote(tag []byte) []byte {

newTag := f.compileMultiLine(`['"](.*)['"]`).FindSubmatch(tag)
if len(newTag) == 2 {
return newTag[1]
}
return tag
}

func (f Filler) unquote(tag []byte) []byte {

return bytes.Trim(tag, `'"`)
}

BenchmarkUnquote-4        200000         11279 ns/op       41681 B/op         36 allocs/op

BenchmarkUnquote-4 20000000 104 ns/op 32 B/op 1 allocs/op

100倍 /(^o^)\


メモリアロケーションがおおいのは、毎回 regexp.MustCompile していたからなので、init() でコンパイルして保持するように変えた。

PASS

BenchmarkUnquote-4 20000000 106 ns/op 32 B/op 1 allocs/op
BenchmarkGetType-4 100000 18935 ns/op 46944 B/op 75 allocs/op
BenchmarkGetValue-4 100000 19344 ns/op 47216 B/op 78 allocs/op
BenchmarkGetName-4 100000 20656 ns/op 46944 B/op 75 allocs/op
BenchmarkEscapeHTML-4 50000 33839 ns/op 162289 B/op 118 allocs/op
BenchmarkFillInput-4 10000 106380 ns/op 235842 B/op 378 allocs/op
BenchmarkFillTextarea-4 20000 90014 ns/op 264868 B/op 321 allocs/op
BenchmarkFillSelect-4 10000 223276 ns/op 448898 B/op 874 allocs/op
BenchmarkFillOption-4 20000 84422 ns/op 193856 B/op 342 allocs/op
BenchmarkFillinForm-4 2000 966790 ns/op 1996791 B/op 3283 allocs/op
ok github.com/sheercat/fillinform 21.391s

PASS
BenchmarkUnquote-4 20000000 105 ns/op 32 B/op 1 allocs/op
BenchmarkGetType-4 1000000 1256 ns/op 112 B/op 3 allocs/op
BenchmarkGetValue-4 1000000 1968 ns/op 112 B/op 3 allocs/op
BenchmarkGetName-4 500000 2673 ns/op 112 B/op 3 allocs/op
BenchmarkEscapeHTML-4 300000 4455 ns/op 880 B/op 26 allocs/op
BenchmarkFillInput-4 100000 16124 ns/op 624 B/op 15 allocs/op
BenchmarkFillTextarea-4 200000 7069 ns/op 624 B/op 14 allocs/op
BenchmarkFillSelect-4 100000 20599 ns/op 1040 B/op 24 allocs/op
BenchmarkFillOption-4 300000 5456 ns/op 352 B/op 10 allocs/op
BenchmarkFillinForm-4 10000 128107 ns/op 9136 B/op 113 allocs/op
ok github.com/sheercat/fillinform 16.768s

当然ですが、メモリアロケーションが減って速くなった。/(^o^)\


regexp.MustCompile を減らすついでに escape html を bytes.Replace に

func (f Filler) escapeHTML(tag string) string {

tag = regexp.MustCompile(`&`).ReplaceAllString(tag, `&`)
tag = regexp.MustCompile(`<`).ReplaceAllString(tag, `&lt;`)
tag = regexp.MustCompile(`>`).ReplaceAllString(tag, `&gt;`)
tag = regexp.MustCompile(`"`).ReplaceAllString(tag, `&quot;`)
return tag
}

func (f Filler) escapeHTML(tag []byte) []byte {

return bytes.Replace(bytes.Replace(bytes.Replace(bytes.Replace(tag, []byte{'&'}, BAAmp, -1), []byte{'<'}, BALt, -1), []byte{'>'}, BAGt, -1), []byte{'"'}, BAQuot, -1)
}

BenchmarkEscapeHTML-4     300000          4455 ns/op         880 B/op         26 allocs/op

BenchmarkEscapeHTML-4 1000000 1004 ns/op 336 B/op 4 allocs/op

たいそう読みづらいけど、速くなった。/(^o^)\


結果 (10000回回した結果)

$carton exec perl bench.pl

14.766043 at bench.pl line 1079.
$go run bench.go
37.802735974s

2.6倍遅いってとこまでこれたー。ワーイ/(^o^)\

パッケージの仕様としてはまだ全然機能が足りない気もしますし、utf-8なのに bytes.Replace, bytes.Trim で処理していいんだっけ、とかそもそもフィルインする仕様が HTML::FillInForm::Lite とはちょっと違っています。

http.Request.PostForm をそのまま渡して動くようにしたかったため、渡ってこないパラメータに関しては空が指定されたかのように動きます。このへんは何が正しいのかよくわからない。ですね。


書いてる過程で知ったこと


リテラル

formData := map[string][]string{

"gender": []string{"1"},
}

formData := map[string][]string{

"gender": {"1"},
}

でいい。これは、1.5 からですかね。


3つ以上のスライスを append する

a := []byte("X")

b := []byte("Y")
c := []byte("Z")
d := append(a, append(b, c...)...)

もっとスマーフな方法なかろうか?

って思ってこういうの書いてみたものの

func appendMultipleByteSlices(bSlices ...[]byte) []byte {

i := 0
for _, b := range bSlices {
i = i + len(b)
}
capped := make([]byte, 0, i)
for _, b := range bSlices {
capped = append(capped, b...)
}

return capped
}

2回ループするとか微妙なので、結局普通に append ネストしました。


文字列リテラル中に変数を展開する方法

ない

プラス + で繋いで作るしか無い...


perl の /.../msi は

(?msi: ... )

ちなみに //x に相当するものはありません。


pprof は mac os x では動かない

動きません。カーネルにパッチあてればいいそうですが、やりませんでした。

https://github.com/golang/go/issues/6047


その他

@lestrrat さんの go-pcre2 が git に上がってたので、機会をみて置き換えてみたいです。