jsonpbはencoding/jsonより5倍遅い


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かけばなんとかなるけど都度書かないといけないし実装もがんばらないと遅い