この記事は HRBrain Advent Calendar 2021 2日目の記事です。
はじめに
Goで構造体をバイナリエンコーディングする際、大抵は encoding/json
か encoding/gob
を使うと思います。
以下の記事を読んで、標準の encoding/binary
パッケージを使えば任意の構造体のエンコード・デコード処理を自前で書けることを知りました。
構造体のエンコード・デコード処理を自前で書くとどれくらい速くなるのか気になったので試してみました。
単純にreflectionを使わない分速くなりそうです。自前で書いたエンコード・デコードのパフォーマンスを、 encoding/json
encoding/gob
と比較してみます。
※最初に言っておきますが実用性は殆ど無いと思います。
型ごとのエンコード・デコード方法
int 系
上で紹介した記事のように encoding/binary
の関数をそのまま使えます。
func IntEncode(i int) ([]byte, int) { // intの変換に必要な最大容量を確保 out := make([]byte, binary.MaxVarintLen64) n := binary.PutVarint(out, int64(i)) return out[:n], n } func IntDecode(in []byte) (int, int) { intRaw, intLen := binary.Varint(in) return int(intRaw), intLen }
int8やint16などもint64にキャストすれば同じ方法で変換できます。
uint 系
intとほぼ同じです。
func UintEncode(u uint) ([]byte, int) { // uintの変換に必要な最大容量を確保 out := make([]byte, binary.MaxVarintLen64) n := binary.PutUvarint(out, uint64(u)) return out[:n], n } func UintDecode(in []byte) (uint, int) { uintRaw, uintLen := binary.Uvarint(in) return uint(uintRaw), uintLen }
float 系
math
パッケージを使います。
func FloatEncode(f float64) ([]byte, int) { out := make([]byte, binary.MaxVarintLen64) n := binary.PutUvarint(out, math.Float64bits(f)) return out[:n], n } func FloatDecode(in []byte) (float64, int) { floatRaw, floatLen := binary.Uvarint(in) return math.Float64frombits(floatRaw), floatLen }
string
文字列のバイト数と文字列を別々にセットします。エンコード時にバイト数をセットすることで、デコード時に読み取るバイト数が分かるようになります。
func StringEncode(str string) ([]byte, int) { n := 0 strSize := len(str) // 文字列の長さの最大 + 文字列の長さ out := make([]byte, binary.MaxVarintLen64+strSize) // 文字列の長さをセット n += binary.PutUvarint(out, uint64(strSize)) // 文字列をコピー copy(out[n:n+strSize], str) n += strSize return out[:n], n } func StringDecode(in []byte) (string, int) { n := 0 // 文字列の長さを読み取る strLen, strLenLen := binary.Uvarint(in) n += strLenLen // 長さの分だけ文字列として読み取る str := string(in[n : n+int(strLen)]) n += int(strLen) return str, n }
bool
trueなら1、falseなら0をセットします。
func BoolEncode(b bool) ([]byte, int) { if b { return []byte{1}, 1 } return []byte{0}, 1 } func BoolDecode(in []byte) (bool, int) { return in[0] == 1, 1 }
01をセットするのに1バイト使っているのが少しもったいない(1ビットで十分のはず)気もします。
ポインタ
先頭1バイトにnilかどうかをセットします。
func PointerEncode(p *int) ([]byte, int) { n := 1 if p == nil { // nilの場合は1バイト目に0をセット return []byte{0}, n } // nil判定 + intの最大 out := make([]byte, n+binary.MaxVarintLen64) // nilでない場合は1バイト目に1をセット out[0] = 1 n += binary.PutVarint(out[n:], int64(*p)) return out[:n], n } func PointerDecode(in []byte) (*int, int) { n := 0 // 1バイト目が0ならnilを返す isNotNil, isNotNilLen := binary.Uvarint(in[n:]) n += isNotNilLen if isNotNil == 0 { return nil, n } intRaw, intLen := binary.Varint(in[n:]) n += intLen t := int(intRaw) return &t, n }
スライス
1バイト目にnilかどうかをセットし、次にスライスの長さをセットし、その後に各要素を詰めていきます。スライスの長さをセットするのは、デコード時にスライスの長さ分ループして読み取るためです。
func SliceEncode(ints []int) ([]byte, int) { n := 1 if ints == nil { // nilの場合は1バイト目に0をセット return []byte{0}, n } intsSize := len(ints) // nil判定 + スライスの長さの最大 + スライスの要素数*1要素の最大 out := make([]byte, n+binary.MaxVarintLen64+intsSize*binary.MaxVarintLen64) // nilでない場合は1バイト目に1をセット out[0] = 1 // スライスの長さをセット n += binary.PutUvarint(out[n:], uint64(intsSize)) // スライスを1要素ずつセット for _, i := range ints { n += binary.PutVarint(out[n:], int64(i)) } return out[:n], n } func SliceDecode(in []byte) ([]int, int) { n := 0 // 1バイト目が0ならnilを返す sliceIsNotNil, sliceIsNotNilLen := binary.Uvarint(in[n:]) n += sliceIsNotNilLen if sliceIsNotNil == 0 { return nil, n } // スライスの長さを読み取る sliceLen, sliceLenLen := binary.Uvarint(in[n:]) n += sliceLenLen slice := make([]int, sliceLen) // 長さの回数だけ読み取る for i := 0; i < int(sliceLen); i++ { intRaw, intLen := binary.Varint(in[n:]) slice[i] = int(intRaw) n += intLen } return slice, n }
time.Time
MarshalBinary
と UnmarshalBinary
というメソッドが標準で用意されているので、これをそのまま使えます。
pkg.go.dev
任意の構造体に実装する
以上の方法で任意の構造体をエンコード・デコードします。例えば以下のような構造体があった場合、
type TestStruct struct { Str string Bool bool Int int Int16 int16 Int64 int64 Uint uint Uint8 uint8 Uint32 uint32 Time time.Time }
エンコード・デコードは以下のように実装できます。
const ( MaxVarintLen8 = 2 VarintLenBool = 1 VarintLenTime = 15 VarintLenPointer = 1 ) func (s TestStruct) Encode() ([]byte, error) { // エンコードに必要な最大サイズを計算する size := 0 // Str size += binary.MaxVarintLen64 size += len(s.Str) // Bool size += VarintLenBool // Int size += binary.MaxVarintLen64 // Int16 size += binary.MaxVarintLen16 // Int64 size += binary.MaxVarintLen64 // Uint size += binary.MaxVarintLen64 // Uint8 size += MaxVarintLen8 // Uint32 size += binary.MaxVarintLen32 // Time size += VarintLenTime // 最大サイズ分の大きさを確保 out := make([]byte, size) n := 0 // Str strSize := len(s.Str) n += binary.PutUvarint(out[n:], uint64(strSize)) copy(out[n:n+strSize], s.Str) n += strSize // Bool if s.Bool { n += binary.PutUvarint(out[n:], uint64(1)) } else { n += binary.PutUvarint(out[n:], uint64(0)) } // Int n += binary.PutVarint(out[n:], int64(s.Int)) // Int16 n += binary.PutVarint(out[n:], int64(s.Int16)) // Int64 n += binary.PutVarint(out[n:], s.Int64) // Uint n += binary.PutUvarint(out[n:], uint64(s.Uint)) // Uint8 n += binary.PutUvarint(out[n:], uint64(s.Uint8)) // Uint32 n += binary.PutUvarint(out[n:], uint64(s.Uint32)) // Time timeBytes, err := s.Time.MarshalBinary() if err != nil { return nil, err } copy(out[n:n+VarintLenTime], timeBytes) n += VarintLenTime return out[:n], nil } func (s *TestStruct) Decode(in []byte) (int, error) { n := 0 // Str strLen, strLenLen := binary.Uvarint(in) n += strLenLen s.Str = string(in[n : n+int(strLen)]) n += int(strLen) // Bool boolRaw, boolLen := binary.Uvarint(in[n:]) s.Bool = boolRaw == 1 n += boolLen // Int intRaw, intLen := binary.Varint(in[n:]) s.Int = int(intRaw) n += intLen // Int16 int16Raw, int16Len := binary.Varint(in[n:]) s.Int16 = int16(int16Raw) n += int16Len // Int64 int64Raw, int64Len := binary.Varint(in[n:]) s.Int64 = int64(int64Raw) n += int64Len // Uint uintRaw, uintLen := binary.Uvarint(in[n:]) s.Uint = uint(uintRaw) n += uintLen // Uint8 uint8Raw, uint8Len := binary.Uvarint(in[n:]) s.Uint8 = uint8(uint8Raw) n += uint8Len // Uint32 uint32Raw, uint32Len := binary.Uvarint(in[n:]) s.Uint32 = uint32(uint32Raw) n += uint32Len // Time err := s.Time.UnmarshalBinary(in[n : n+VarintLenTime]) if err != nil { return 0, err } n += VarintLenTime return n, nil }
以下のコードでテストしてみます。
data := TestStruct{ Str: "test_string", Bool: true, Int: 1, Int16: 10000, Int64: 1000000000000000000, Uint: 1, Uint8: 100, Uint32: 1000000000, Time: time.Now(), } bytes, _ := data.Encode() fmt.Println(len(bytes)) decoded := TestStruct{} decoded.Decode(bytes) if diff := cmp.Diff(data, decoded); diff != "" { fmt.Println(diff) } else { fmt.Println("no diff") }
実行すると以下の出力になり、正常にエンコード・デコードできていそうです。
48 no diff
パフォーマンス比較
以下の構造体のスライス TestStructs
を使って計測します。
type TestStruct struct { Str string Bool bool Int int Int16 int16 Int64 int64 Uint uint Uint8 uint8 Uint32 uint32 Time time.Time SubPointer *TestSubStruct Subs TestSubStructs } type TestStructs []TestStruct type TestSubStruct struct { Str string Bool bool Int int Int16 int16 Int64 int64 Uint uint Uint8 uint8 Uint32 uint32 Time time.Time } type TestSubStructs []TestSubStruct
エンコード・デコードの処理はかなり長いので興味がある方は以下を見てください。
テスト用のスライスをランダムに生成し、エンコード・デコードのベンチマークを測定します。
func createTestStructs(size int) TestStructs { ss := make(TestStructs, size) for i := range ss { subs := make(TestSubStructs, 10) for j := range subs { subs[j] = createTestSubStruct() } sub := createTestSubStruct() ss[i] = TestStruct{ Str: randString(10), Bool: rand.Int()%2 == 1, Int: rand.Int(), Int16: int16(rand.Int()), Int64: int64(rand.Int()), Uint: uint(rand.Uint64()), Uint8: uint8(rand.Uint64()), Uint32: uint32(rand.Uint64()), Time: time.Now(), SubPointer: &sub, Subs: subs, } } return ss } func createTestSubStruct() TestSubStruct { return TestSubStruct{ Str: randString(10), Bool: rand.Int()%2 == 1, Int: rand.Int(), Int16: int16(rand.Int()), Int64: int64(rand.Int()), Uint: uint(rand.Uint64()), Uint8: uint8(rand.Uint64()), Uint32: uint32(rand.Uint64()), Time: time.Now(), } } const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" func randString(n int) string { b := make([]byte, n) for i := range b { b[i] = letters[int(rand.Int63()%int64(len(letters)))] } return string(b) }
スライスの長さを変えて測ってみます。テストコードは以下です。
結果は以下になりました。
スライスの長さが10,000の場合、
- エンコード速度:jsonの26倍、gobの3.7倍
- デコード速度:jsonの20倍、gobの2.4倍
になりました。メモリ消費も0.2 ~ 0.6倍に抑えられています。
スライスの長さが1の場合、
- エンコード速度:jsonの8.6倍、gobの9.7倍
- デコード速度:jsonの22倍、gobの18倍
となり、メモリ消費は0.1 ~ 0.4倍です。gobは単一の構造体が苦手らしいので、その影響が出ていそうです。
ついでにスライスの長さ10,000の場合のエンコード後のバイト列の長さを測ってみました。
23981425 8846815 7790238
上からjson、gob、自前です。
もう少し頑張ってみる
time.Time
のエンコード時にメモリのコピーが発生しているので、これを無くせばもう少し速くなりそうです。そこで、引数で受け取ったバイト列にエンコード結果をセットできるような関数を用意しました。
var timeZero = time.Time{}.Unix() func TimeMarshalBinary(t time.Time, out []byte) (int, error) { var offsetMin int16 // minutes east of UTC. -1 is UTC. if t.Location() == time.UTC { offsetMin = -1 } else { _, offset := t.Zone() if offset%60 != 0 { return 0, errors.New("Time.MarshalBinary: zone offset has fractional minute") } offset /= 60 if offset < -32768 || offset == -1 || offset > 32767 { return 0, errors.New("Time.MarshalBinary: unexpected zone offset") } offsetMin = int16(offset) } unix := t.Unix() sec := unix - timeZero nsec := t.UnixNano() - unix*1000000000 out[0] = 1 // byte 0 : version out[1] = byte(sec >> 56) // bytes 1-8: seconds out[2] = byte(sec >> 48) out[3] = byte(sec >> 40) out[4] = byte(sec >> 32) out[5] = byte(sec >> 24) out[6] = byte(sec >> 16) out[7] = byte(sec >> 8) out[8] = byte(sec) out[9] = byte(nsec >> 24) // bytes 9-12: nanoseconds out[10] = byte(nsec >> 16) out[11] = byte(nsec >> 8) out[12] = byte(nsec) out[13] = byte(offsetMin >> 8) // bytes 13-14: zone offset in minutes out[14] = byte(offsetMin) return VarintLenTime, nil }
既存の Time.MarshalBinary
をコピペして少しいじっただけです。time.Time
のエンコードをこれに差し替えました。
結果は以下になります。
allocateがなくなった分、1.2倍ほど速くなりました。長さ10,000の場合、jsonの31倍、gobの4.4倍です。
2021/12/04追記
「Protocol Buffers と比較したらどうなるの?」っと聞かれたので試してみました。
その時におすすめされた以下を使います。速いらしいです。
任意の構造体のエンコードはできないので、テストに使った構造体と同じようなprotoファイルを用意しました。
syntax = "proto3"; package proto; import "google/protobuf/timestamp.proto"; message TestStructs { repeated TestStruct ss = 1; } message TestStruct { string str = 1; bool bool = 2; int64 int = 3; int32 int16 = 4; int64 int64 = 5; uint64 uint = 6; uint32 uint8 = 7; uint32 uint32 = 8; google.protobuf.Timestamp time = 9; TestSubStruct sub_pointer = 10; repeated TestSubStruct subs = 11; } message TestSubStruct { string str = 1; bool bool = 2; int64 int = 3; int32 int16 = 4; int64 int64 = 5; uint64 uint = 6; uint32 uint8 = 7; uint32 uint32 = 8; google.protobuf.Timestamp time = 9; }
Goのコードを生成します。
# protoc -I=. -I=$GOPATH/src -I=$GOPATH/src/github.com/gogo/protobuf/protobuf --gogofaster_out=Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types:. struct.proto
TestStructs
から proto.TestStructs
へ変換する関数を用意し、proto.TestStructs
のエンコード・デコードのベンチマークをとります。(「任意の構造体」ではないので参考値ですが)
結果は以下になりました。
めっちゃ速いです。エンコードは自前とほぼ同じです。デコードはそこまでですが、それでもgobより速いです。自動生成でここまで速いとはすごいですね。
まとめ
頑張ったらエンコードはgobの4.4倍、デコードはgobの2.4倍の速度になりました。逆に頑張ってもそれくらいの速度改善にしかならないので、よほどエンコード・デコードのパフォーマンスで困っていない限りgobで良いな、と思いました。もっと単純な構造体なら実装も楽なので採用しても良いかもしれません。
もっと速くなる案があれば教えていただきたいです。
最後にベンチマークの結果をそのまま載せておきます。
# go test -benchmem -bench=. . goos: linux goarch: amd64 pkg: encode cpu: Intel(R) Core(TM) i7-8557U CPU @ 1.70GHz Benchmark_encode_____json_____1-4 82219 14040 ns/op 3288 B/op 14 allocs/op Benchmark_encode______gob_____1-4 76011 15790 ns/op 7296 B/op 93 allocs/op Benchmark_encode_____self_____1-4 772065 1633 ns/op 1216 B/op 13 allocs/op Benchmark_encode_selftime_____1-4 945372 1314 ns/op 1024 B/op 1 allocs/op Benchmark_encode_____json____10-4 8667 135845 ns/op 30371 B/op 122 allocs/op Benchmark_encode______gob____10-4 15444 89294 ns/op 53728 B/op 317 allocs/op Benchmark_encode_____self____10-4 72669 16511 ns/op 11392 B/op 121 allocs/op Benchmark_encode_selftime____10-4 89332 13599 ns/op 9472 B/op 1 allocs/op Benchmark_encode_____json___100-4 870 1442269 ns/op 303406 B/op 1202 allocs/op Benchmark_encode______gob___100-4 1879 624703 ns/op 523552 B/op 2485 allocs/op Benchmark_encode_____self___100-4 7363 165785 ns/op 117504 B/op 1201 allocs/op Benchmark_encode_selftime___100-4 8685 164211 ns/op 98304 B/op 1 allocs/op Benchmark_encode_____json__1000-4 75 14407019 ns/op 2976444 B/op 12002 allocs/op Benchmark_encode______gob__1000-4 181 7098993 ns/op 6026404 B/op 24095 allocs/op Benchmark_encode_____self__1000-4 633 1835921 ns/op 1117696 B/op 12001 allocs/op Benchmark_encode_selftime__1000-4 790 1509317 ns/op 925696 B/op 1 allocs/op Benchmark_encode_____json_10000-4 7 459676600 ns/op 40510650 B/op 120008 allocs/op Benchmark_encode______gob_10000-4 16 65469900 ns/op 58843049 B/op 240105 allocs/op Benchmark_encode_____self_10000-4 58 17858890 ns/op 11160576 B/op 120001 allocs/op Benchmark_encode_selftime_10000-4 72 14867447 ns/op 9240576 B/op 1 allocs/op Benchmark_decode_____json_____1-4 24745 48605 ns/op 5632 B/op 115 allocs/op Benchmark_decode______gob_____1-4 30276 39866 ns/op 13637 B/op 348 allocs/op Benchmark_decode_____self_____1-4 585103 2236 ns/op 1312 B/op 15 allocs/op Benchmark_decode_selftime_____1-4 588283 2233 ns/op 1312 B/op 15 allocs/op Benchmark_decode_____json____10-4 2521 482171 ns/op 52616 B/op 1063 allocs/op Benchmark_decode______gob____10-4 12969 94039 ns/op 34814 B/op 492 allocs/op Benchmark_decode_____self____10-4 48012 23574 ns/op 13120 B/op 141 allocs/op Benchmark_decode_selftime____10-4 46819 22666 ns/op 13120 B/op 141 allocs/op Benchmark_decode_____json___100-4 254 4856967 ns/op 534768 B/op 10490 allocs/op Benchmark_decode______gob___100-4 2083 581402 ns/op 240940 B/op 1932 allocs/op Benchmark_decode_____self___100-4 5622 235138 ns/op 130688 B/op 1401 allocs/op Benchmark_decode_selftime___100-4 5599 234787 ns/op 130688 B/op 1401 allocs/op Benchmark_decode_____json__1000-4 24 47391775 ns/op 5201429 B/op 104563 allocs/op Benchmark_decode______gob__1000-4 201 5501483 ns/op 2290962 B/op 16332 allocs/op Benchmark_decode_____self__1000-4 520 2327776 ns/op 1306881 B/op 14001 allocs/op Benchmark_decode_selftime__1000-4 514 2337885 ns/op 1306881 B/op 14001 allocs/op Benchmark_decode_____json_10000-4 3 468327567 ns/op 52413248 B/op 1045349 allocs/op Benchmark_decode______gob_10000-4 20 56158780 ns/op 22782913 B/op 160332 allocs/op Benchmark_decode_____self_10000-4 48 23602694 ns/op 13044240 B/op 140001 allocs/op Benchmark_decode_selftime_10000-4 46 22891991 ns/op 13044240 B/op 140001 allocs/op
2021/12/04追記
Protocol Buffers を追加して測り直した分です。
Benchmark_encode_____json_____1-4 80320 14865 ns/op 3288 B/op 14 allocs/op Benchmark_encode______gob_____1-4 62854 16038 ns/op 7296 B/op 93 allocs/op Benchmark_encode_____self_____1-4 754052 1651 ns/op 1216 B/op 13 allocs/op Benchmark_encode_selftime_____1-4 916267 1369 ns/op 1024 B/op 1 allocs/op Benchmark_encode_protobuf_____1-4 803013 1607 ns/op 912 B/op 2 allocs/op Benchmark_encode_____json____10-4 8752 141980 ns/op 30370 B/op 122 allocs/op Benchmark_encode______gob____10-4 17295 74739 ns/op 53728 B/op 317 allocs/op Benchmark_encode_____self____10-4 63856 17466 ns/op 11392 B/op 121 allocs/op Benchmark_encode_selftime____10-4 83157 13136 ns/op 9472 B/op 1 allocs/op Benchmark_encode_protobuf____10-4 96116 16727 ns/op 9488 B/op 2 allocs/op Benchmark_encode_____json___100-4 823 1392776 ns/op 304117 B/op 1202 allocs/op Benchmark_encode______gob___100-4 1957 673258 ns/op 523552 B/op 2485 allocs/op Benchmark_encode_____self___100-4 6825 201729 ns/op 117504 B/op 1201 allocs/op Benchmark_encode_selftime___100-4 6709 156873 ns/op 98304 B/op 1 allocs/op Benchmark_encode_protobuf___100-4 8925 133352 ns/op 90128 B/op 2 allocs/op Benchmark_encode_____json__1000-4 76 14612655 ns/op 3100622 B/op 12002 allocs/op Benchmark_encode______gob__1000-4 171 6456594 ns/op 6026403 B/op 24095 allocs/op Benchmark_encode_____self__1000-4 688 2182507 ns/op 1117696 B/op 12001 allocs/op Benchmark_encode_selftime__1000-4 632 1678611 ns/op 925696 B/op 1 allocs/op Benchmark_encode_protobuf__1000-4 667 1515424 ns/op 901136 B/op 2 allocs/op Benchmark_encode_____json_10000-4 7 155124814 ns/op 29746667 B/op 120004 allocs/op Benchmark_encode______gob_10000-4 18 64874194 ns/op 58843053 B/op 240105 allocs/op Benchmark_encode_____self_10000-4 69 18463733 ns/op 11160578 B/op 120001 allocs/op Benchmark_encode_selftime_10000-4 73 14718711 ns/op 9240576 B/op 1 allocs/op Benchmark_encode_protobuf_10000-4 70 16400381 ns/op 9003027 B/op 2 allocs/op Benchmark_decode_____json_____1-4 24993 48118 ns/op 5632 B/op 115 allocs/op Benchmark_decode______gob_____1-4 27406 38389 ns/op 13637 B/op 348 allocs/op Benchmark_decode_____self_____1-4 580430 2222 ns/op 1312 B/op 15 allocs/op Benchmark_decode_selftime_____1-4 591904 2192 ns/op 1312 B/op 15 allocs/op Benchmark_decode_protobuf_____1-4 316608 3711 ns/op 2056 B/op 44 allocs/op Benchmark_decode_____json____10-4 2572 484292 ns/op 52616 B/op 1063 allocs/op Benchmark_decode______gob____10-4 13778 91646 ns/op 34813 B/op 492 allocs/op Benchmark_decode_____self____10-4 53476 22444 ns/op 13120 B/op 141 allocs/op Benchmark_decode_selftime____10-4 55375 23805 ns/op 13120 B/op 141 allocs/op Benchmark_decode_protobuf____10-4 31502 36918 ns/op 20368 B/op 417 allocs/op Benchmark_decode_____json___100-4 256 4851948 ns/op 534768 B/op 10490 allocs/op Benchmark_decode______gob___100-4 2065 580356 ns/op 240942 B/op 1932 allocs/op Benchmark_decode_____self___100-4 5539 239239 ns/op 130688 B/op 1401 allocs/op Benchmark_decode_selftime___100-4 5594 228990 ns/op 130688 B/op 1401 allocs/op Benchmark_decode_protobuf___100-4 3372 380151 ns/op 202880 B/op 4110 allocs/op Benchmark_decode_____json__1000-4 24 46784458 ns/op 5201424 B/op 104563 allocs/op Benchmark_decode______gob__1000-4 213 5805051 ns/op 2290960 B/op 16332 allocs/op Benchmark_decode_____self__1000-4 498 2336990 ns/op 1306880 B/op 14001 allocs/op Benchmark_decode_selftime__1000-4 447 2820632 ns/op 1306880 B/op 14001 allocs/op Benchmark_decode_protobuf__1000-4 272 4246900 ns/op 2024416 B/op 41013 allocs/op Benchmark_decode_____json_10000-4 2 666970100 ns/op 52413264 B/op 1045349 allocs/op Benchmark_decode______gob_10000-4 19 61811889 ns/op 22782912 B/op 160332 allocs/op Benchmark_decode_____self_10000-4 45 27595242 ns/op 13044226 B/op 140001 allocs/op Benchmark_decode_selftime_10000-4 45 31214431 ns/op 13044224 B/op 140001 allocs/op Benchmark_decode_protobuf_10000-4 30 40762980 ns/op 20466339 B/op 410022 allocs/op