薄いブログ

技術の雑多なことを書く場所

go-openapi/swagを速くした話

github.com

GitHub - go-openapi/swag: goodie bag in use in the go-openapi projects を速くしたときの話をしたいと思います.

go-openapi/swag はgo-openapi と go-swagger のヘルパー関数が入っているライブラリです.

調査の結果, regexp.MustCompileが重くToGoName経由でspllitという関数から呼ばれていることがわかっていました.

実際に見てみると以下のような実装になっていました.

// Prepares strings by splitting by caps, spaces, dashes, and underscore
func split(str string) []string {
    repl := strings.NewReplacer(
        "@", "At ",
        "&", "And ",
        "|", "Pipe ",
        "$", "Dollar ",
        "!", "Bang ",
        "-", " ",
        "_", " ",
    )

    rex1 := regexp.MustCompile(`(\p{Lu})`)
    rex2 := regexp.MustCompile(`(\pL|\pM|\pN|\p{Pc})+`)

    str = trim(str)

    // Convert dash and underscore to spaces
    str = repl.Replace(str)

    // Split when uppercase is found (needed for Snake)
    str = rex1.ReplaceAllString(str, " $1")

    // check if consecutive single char things make up an initialism
    once.Do(ensureSorted)
    for _, k := range initialisms {
        str = strings.Replace(str, rex1.ReplaceAllString(k, " $1"), " "+k, -1)
    }
    // Get the final list of words
    //words = rex2.FindAllString(str, -1)
    return rex2.FindAllString(str, -1)
}

見るとわかりますが, regexp.MustCompileが二回呼び出されています.

regexp.MustCompileは重い処理で, 取っている引数が定数なので毎度計算する必要がないことがわかります.

またこれは蛇足ですが, strings.Replacerにおいても同様のことが言えます.

こういったケースにおいて, initでグローバル変数で予め計算したものを保持しておくパターンか,sync.Onceを使って初回に一度だけ計算するパターンのどちらかで対処できます.

initを使う場合は必ず実行されてしまうので起動時に初期化のコストがかかってしまい, sync.Onceの場合は呼び出されたタイミングで実行されるので必要じゃない場合にコストはかからないが呼び出しごとに若干のオーバーヘッドがかかってしまいます. 一長一短なので考えて採用しましょう.

またグローバル変数にして大丈夫かをドキュメントで確認しておきましょう.

そのオブジェクトのメソッドが並列に呼び出すことをサポートしてない場合は上の方針は採用できません.

標準ライブラリの場合は書いてあることが多いように思います.

regexp - The Go Programming Language

A Regexp is safe for concurrent use by multiple goroutines, except for configuration methods, such as Longest.

strings - The Go Programming Language

It is safe for concurrent use by multiple goroutines.

確認して上記方針で問題ないことがわかりました.

今回の場合は, sortの処理だけsync.Onceを用いていたのでそこに乗っかる形で修正しました.

ベンチマークの結果, 3倍程度速くなりました.

まとめ

regexp.MustCompileやstrings.NewReplacerは重い.

重い構築系の関数はsync.Onceやグローバル変数に逃がすことで解決できることがある.

ちゃんと複数のgoroutineで読んでも安全かどうかをdocumentで確認する.

用法用量を守ったチューニングをしていきましょう.