LoginSignup
11

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-12-09

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

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

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

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

ということで、このままだと 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 では動かない

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

その他

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

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
11