My External Storage

Aug 20, 2020 - 3 minute read - Comments -

[Go] パフォーマンスが悪い正規表現パッケージの使い方をチェックするlinterを作った

正規表現パッケージのコンパイルを何度も呼び出していないかチェックするlinterを作った。

TL;DR

regexpパッケージをコンパイルを使うときのお作法

Goの正規表現を使いたいときはregexpパッケージを使う。
このパッケージの使い方には注意すべき点がある。

上記記事の以下の部分が注意点だ。

正規表現オブジェクトのコンパイルはコストのかかる処理であり、これは以下のようにグローバル領域にコンパイル済みの状態で宣言します。 検査時に随時生成することは推奨されません。

たとえば、WebサーバをGoで実装していた場合、リクエストを受け取るたびにコンパイル処理をしているとパフォーマンスが悪い。

func myHandler(w http.ResponseWriter, r *http.Request) {
    // リクエストを受け取るたびに正規表現のコンパイルが走る。
    re := regexp.MustCompile(`(gopher){2}`)
    body, _ := ioutil.ReadAll(resp.Body)
    if re.Match(re) {
        // Do anything...
    }
    // Do anything...
}

正しくは次のように書かないといけない。

// グローバルスコープならばmain goroutin起動時に一度だけの実行で済む。
var re = regexp.MustCompile(`(gopher){2}`)

func myHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := ioutil.ReadAll(resp.Body)
    if re.Match(re) {
        // Do anything...
    }
    // Do anything...
}

このようなことを静的解析で指摘する静的解析ツールを作った。

と、言っても大部分のロジックは@tenntennさんのcalled linterを踏襲している。

「明らかに一度しか実行されない関数」は正しい利用方法だが、検知できない。
そのため誤検知してしまう部分にたいしてはコメントをつけることで無視する機能もつけた(流用させてもらった)。

func f() {
	// lint:ignore regexponce allowed
	validID = regexp.MustCompile(`^[a-z]+\[[0-9]+\]$`) // OK because add specified comment.
}

メインのロジックはanalysisyutilパッケージを使い、作り始めはskeletonコマンドでガッと作っている。便利。

自分で頑張った独自性というところだと、単純にコード内に現れたMuxtCompile関数などをすべてエラーとせずに次の例外を設けている。

  • グローバルスコープ
  • main関数
    • ただし、main関数の中のforループ内で使っていた場合はエラーにする。
  • init関数

どれも起動時に一度しか実行されない部分なので、静的解析のエラーにはしない。

難しかったところ

標準pkgの関数だったので、最初はgo/importer.Default()経由でregexp.MustCompileなどの*types.Funcオブジェクトを取得していた。
が、同じ関数に対するオブジェクトはずなのにコード中に現れるregexp.MustCompileとはマッチしないようだ。posなどの位置も全然違った。
実装を優先したのでちゃんと原因は調べられていない…

終わりに

SSAはなかなか難しい(私がデータ構造をきちんと理解できていない)ので、こちらの本を参考に見様見真似で書いた感じ。
もっと静的解析勉強して実装速度と実装できる手札を増やしていきたい。

参考

関連記事