こんにちは。
長らく 放置していた 更新が滞っていた msgpackgen という OSS を大幅に実装し直してバージョンを v1 にアップグレードしました。(細かいのを除いたら実に5年弱ぶり)

そもそもなぜ作ったのかはこちらの記事をどうぞ。

この記事では変更点について少しお話したいと思います。

v0(初版) のリリースとその後

msgpackgen を公開した当初は、ある程度のものは出来たと思いつつも preview 的な位置づけかなという思いもあり v0 としてリリースをしました。

リリース当初はパフォーマンス的には最速だったのですが、比較対象にしていた tinylib/msgp がパフォーマンス改善を行ったことにより、優位性が薄れてしまいました。

リリース当初は最速でした

リリース当初は最速でした

しかし負けてしまいました

しかし負けてしまいました

これしたら速くならないかな…? これしたら速くならないかな…? みたいなのはたまに試していましたが、あまり目立った効果は出せず更新は滞っていくのでした…

AI の台頭、そしてリトライ、v1 のリリースへ

昨今は AI を使ったソフトウェア開発を盛ん(?)に行われ、スピートが飛躍的に上がっていると思います。かくいう私も便利だな〜と思っているわけで、有名なツールを全部 AI で書き換えたという話は一時話題になりました。

AI に助けてもらったらパフォーマンス改善できるかもしれないということでリトライをすることにしました。現状のコードを分析したところ、色々を改善できる箇所があることがわかりました。

  • Encoder の廃止
  • resolver 登録式の廃止
  • switch string での比較
  • 境界値チェックのスキップ
  • unsafe の利用
  • その他もろもろ…

(unsafe に関しては安心安全をとって不採用にしました)

変更提案は破壊的変更を含んだ大規模なものになることは目に見えていたので、変更 → ベンチマーク → レビュー を細かい単位で行いました。いい感じに実装を作った resolver が allocation を増やしているとは思いませんでした…。

そんなこんなを詰め込んで見た結果、同等の速度まで近づけることができました。(msgp も no unsafe で計測しています)

Encode の計測結果

Encode の計測結果

Decode の計測結果

Decode の計測結果

上記の結果は良好ですが、計測環境によっては筆者の OSS の方が遅い数値が出たりすることもあります。とはいえ 数十ns の差なので、現状はリリースできるレベルだろうということで v1 を公開することにしました。

使い方

v0 との変更点と使い方も紹介しておきます。

コード生成

使い方は基本的には変わっていません。最新バージョンの Go を使っている方であれば go tool を使うのも良いかなと思っています。

# go の tool ディレクティブでツール管理
go get -tool github.com/shamaton/msgpackgen

例えば msgpackgen.go というファイルを作成して、下記のようなディレクティブコメントを書いておきます。

//go:generate go tool github.com/shamaton/msgpackgen

あとはターミナル等で go generate を実行してください。

go generate ./...

実行すると msgpackgen.go があるディレクトリを含め、再帰的に配下のディレクトリまでコード生成可能な構造体(struct)を検出しコード生成を行います。ファイル名は msgpack.msgpackgen.go で生成されます。(option で変更可能です)

実際の例を確認したいという方はベンチマークのリポジトリがあるので参考にしてみてください。

呼び出し方

v1 では resolver 形式ではなくなり、生成コードに呼び出すべき public function が各種生成されるようになりました。

  • Encode
    • Marshal
    • MarshalAsMap
    • MarshalAsArray
  • Decode
    • Unmarshal
    • UnmarshalAsMap
    • UnmarshalAsArray

Marshal/Unmarshal はこれまでと同様に encoding/json パッケージと同じ signature なので、そのまま置き換えが可能です。

// main.go と生成コードが同じ階層にある場合
func main() {
    v := Struct{Int: 1}
    b, err := Marshal(v)
    if err != nil {
        panic(err)
    }

    var vv Struct
    err = msgpack.Unmarshal(b, &vv)
    if err != nil {
        panic(err)
    }
}

fallback はこれまでと同様にmsgpackが呼ばれます。(登録されていないデータが引数に渡された場合の挙動)

今後

まずは少しでも使ってもらえると良さそう、というのと Stream 対応が出来ていないのでそこの実装を進めつつという感じです!