jsonpbはencoding/jsonより5倍遅い

gRPC(protobuf3)ではprotobufとJSONを相互変換するための専用のライブラリとして[jsonpb](https://github.com/golang/protobuf/tree/master/jsonpb)が用意されています。

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

* `Any`や`Timestamp`といったprotobufの[ptypes](https://github.com/golang/protobuf/tree/master/ptypes)で定義されているmessage型に対応
* `oneof`のunionのような型にも対応

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

## ベンチマークの種類

* ライブラリ
 * encoding/json
 * github.com/golang/protobuf/jsonpb
* メッセージ
 * Small(StringMessage)
 * Large(ABitOfEverything)
 * Oneof(SomeValue)
* 処理
 * Marshal
 * Unmarshal

### proto定義

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

```protobuf
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回実行していますが、最適化の余地はあります。

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