Posted at

jsonpbはencoding/jsonより5倍遅い

More than 3 years have passed since last update.

gRPC(protobuf3)ではprotobufとJSONを相互変換するための専用のライブラリとしてjsonpbが用意されています。

標準ライブラリのencoding/jsonとの違いは



  • AnyTimestampといったprotobufのptypesで定義されているmessage型に対応


  • oneofのunionのような型にも対応

しかしライブラリ内部でいろいろとチェックしている分遅いという話があったのでベンチマークをとってみました。


ベンチマークの種類


  • ライブラリ


    • encoding/json

    • github.com/golang/protobuf/jsonpb



  • メッセージ


    • Small(StringMessage)

    • Large(ABitOfEverything)

    • Oneof(SomeValue)



  • 処理


    • Marshal

    • Unmarshal




proto定義

メッセージに使うproto定義は以下の通り。ABitOfEverythingはgrpc-gatewayのものを少し修正して使用。

message StringMessage {

string Value = 1;
}

message ABitOfEverything {
message Nested {
string name = 1;
uint32 amount = 2;
enum DeepEnum {
FALSE = 0;
TRUE = 1;
}
DeepEnum ok = 3;
}

string uuid = 1;
repeated Nested nested = 2;
float float_value = 3;
double double_value = 4;
int64 int64_value = 5;
uint64 uint64_value = 6;
int32 int32_value = 7;
fixed64 fixed64_value = 8;
fixed32 fixed32_value = 9;
bool bool_value = 10;
string string_value = 11;
uint32 uint32_value = 13;
sfixed32 sfixed32_value = 15;
sfixed64 sfixed64_value = 16;
sint32 sint32_value = 17;
sint64 sint64_value = 18;
repeated string repeated_string_value = 19;
}

message SomeValue {
int32 Type = 1;
oneof Value {
int32 num = 2;
string str = 3;
bool b = 4;
}
}


Marshal

Anyやoneofなどを使っていなければencoding/jsonでも普通にmarshal/unmarshal可能です。

oneofを使っている場合でもjsonpbとmarshal後の結果が異なりますが、marshal自体は可能です。

$ go test -benchtime 10s -benchmem -bench Marshal

BenchmarkBuiltinMarshalSmall-4 10000000 1200 ns/op 184 B/op 2 allocs/op
BenchmarkJSONPBMarshalSmall-4 3000000 5882 ns/op 672 B/op 13 allocs/op
BenchmarkBuiltinMarshalLarge-4 1000000 11799 ns/op 1288 B/op 5 allocs/op
BenchmarkJSONPBMarshalLarge-4 200000 84501 ns/op 9408 B/op 182 allocs/op
BenchmarkBuiltinMarshalOneof-4 10000000 2025 ns/op 192 B/op 3 allocs/op
BenchmarkJSONPBMarshalOneof-4 1000000 10955 ns/op 1296 B/op 26 allocs/op


  • BultinとJSONPBで5倍の差がある

  • largeになると差が7倍以上になる

  • oneofでは5倍のまま


Unmarshal

encoding/jsonでoneofを使ったunmarshalは通常ではできないので、自分でUnmarshalJSONを定義してunmarshalできるようにしました。単純に実装しただけなのでjson.Unmarshalを3回実行していますが、最適化の余地はあります。

func _SomeValue_UnmarshalJSON(data []byte) (isSomeValue_Value, error) {

raw := make(map[string]json.RawMessage)
err := json.Unmarshal(data, &raw)
if err != nil {
return nil, err
}

var s isSomeValue_Value
for k, v := range raw {
switch k {
case "Num":
s = &SomeValue_Num{}
case "Str":
s = &SomeValue_Str{}
case "B":
s = &SomeValue_B{}
default:
return nil, fmt.Errorf("error key: %s, val: %s\n", k, v)
}

err := json.Unmarshal(data, s)
if err != nil {
return nil, err
}
return s, nil
}
return nil, nil
}

func (m *SomeValue) UnmarshalJSON(data []byte) error {
raw := make(map[string]json.RawMessage)
err := json.Unmarshal(data, &raw)
if err != nil {
return err
}
for k, v := range raw {
switch k {
case "type", "Type":
err := json.Unmarshal(v, &m.Type)
if err != nil {
return err
}
case "Value", "value":
d, err := _SomeValue_UnmarshalJSON(v)
if err != nil {
return err
}
m.Value = d
}
}
return nil
}

$ go test -benchtime 10s -benchmem -bench Unmarshal

BenchmarkBuiltinUnmarshalSmall-4 5000000 2641 ns/op 256 B/op 3 allocs/op
BenchmarkJSONPBUnmarshalSmall-4 1000000 13145 ns/op 2040 B/op 21 allocs/op
BenchmarkBuiltinUnmarshalLarge-4 500000 26871 ns/op 496 B/op 15 allocs/op
BenchmarkJSONPBUnmarshalLarge-4 200000 109418 ns/op 9657 B/op 129 allocs/op
BenchmarkBuiltinUnmarshalOneof-4 1000000 23897 ns/op 2416 B/op 38 allocs/op
BenchmarkJSONPBUnmarshalOneof-4 1000000 20067 ns/op 2408 B/op 30 allocs/op


  • BuiltinとJSONPBの差はsmallでもlargeでも5倍

  • oneofでは実装したUnmarshalerがしょぼいのでJSONPBよりも遅くなった


まとめ


  • jsonpbはencoding/jsonより5倍遅い

  • anyやoneofなどを使っている場合は現状jsonpbを使うしかない

  • 自力でunmarshalerかけばなんとかなるけど都度書かないといけないし実装もがんばらないと遅い