11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GoAdvent Calendar 2015

Day 10

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 に上がってたので、機会をみて置き換えてみたいです。

11
11
6

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
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?