きっかけ
generic というジェネリックスもどきな独自型をGoで作っているのですが、~~現在大幅な変更を行っており、~~行いました。そこでベンチマークを取った際にMarshalJSONとUnmarshalJSONのパフォーマンスが悪いことがわかりました。
実装の違い
ベンチマーク
変更前がversion 1.0.0
変更後がversion 2.0.0
version | requests | /op | B/op | allocs/op |
---|---|---|---|---|
1.0.0 | 5000000 | 240 ns | 185 | 3 |
2.0.0 | 200000000 | 6.69 ns | 0 | 0 |
変更前(version 1.0.0)
よく紹介されるMarshalJSONのような感じで書いていました。
// Bool is generic boolean type structure
type Bool struct {
ValidFlag
bool bool
}
// MarshalJSON implements the json.Marshaler interface.
func (v Bool) MarshalJSON() ([]byte, error) {
if !v.Valid() {
return json.Marshal(nil)
}
return json.Marshal(v.Bool)
}
変更後(version 2.0.0)
※ Bool.Boolが、Bool.boolがになったのはこの記事の件とは関係ないので気にしないでください
MarshalJSONでどのようにjsonにencodeしたいのか、はっきりしているので、json.Marshal
で処理せずに自分で処理を書いて対応しています。
// Bool is generic boolean type structure
type Bool struct {
ValidFlag
bool bool
}
// MarshalJSON implements the json.Marshaler interface.
func (v Bool) MarshalJSON() ([]byte, error) {
if !v.Valid() {
return nullBytes, nil
}
if v.bool {
return []byte("true"), nil
}
return []byte("false"), nil
}
なぜパフォーマンスが改善したのか?
JSONのエンコードの処理はkey/valueが[]byte
で管理できるようになるまで、interfaceもしくは、reflect.Valueで管理されることになります。
必要に応じて型判定を繰り返し、json.Marshalerを継承していることが判定されると、marshalerEncoder
という関数が呼ばれ、その中で実装したMarshalJSONを呼ばれる事になります。
この中で、json.Marshalを使った場合、今まで行ってきた処理の後続処理ではなく、上記に書いてきたことを1から行い、[]byteに変換できるか、nilもしくはGoで定義済みの型に変換できるになるまで、これを繰り返すことになります。
TODO: encoding/json
の実装を後で詳しく書く。
参考にしたpackage
goのtime
は様々なMarshaler/Unmarshalerが用意されており、コメントも充実していたのでとても参考になりました。
https://golang.org/src/time/time.go
UnmarshalJSONは対応しないの?
UnmarhshalJSONも対応を考えたのですが、以下のことを理由に対応を見送りました。
- jsonかどうかの判定
- 入力値/型は不明確
jsonかどうかの判定
これが一番のボトルネックだと思います。
Go 1.9 には、encoding/json
にValid
というJSONエンコーディングが正しいのか判定する関数がありますが、1.8以下ではこれがありません。
それまで、encoding/json
にこういった処理がまったくなかったかと言うとそうではなく、privateな関数になってました。
そのため 1.8未満でJSONエンコーディングの判定をしようとすると、encoding/json
にあった処理と同等な処理を自前で実装する事になります。
OSSで公開していない場合ならそれでもいいのかもしれませんが、そういった都合上で1.8以下を切るといった判定でできないので、json.Unmarshal
に任せることにしました。
入力値/型は不明確
MarshalJSONはstructureが保持している値を変換してあげるだけなので、何をどう変換すればいいかが明確です。
それに対し、UnmarhshalJSONはどんな値がどんな型で渡ってくるかがわかりません。
なので、こちらが想定している値/型が渡ってきていることを判定する必要がありますが、自分が作っていたものが様々な型の値を許容していたので、メンテナンスコストを考えて保留としました。
型や値がある程度絞られるようだったら、UnmarhshalJSONでもjson.Unmarshalした方が良いかと思います。