Gob vs JSON
JSONよりGobの方が…
データのシリアライズのフォーマットとしてはJSONがよく使われるが、受取先もGoならGobというパッケージが使える。しかもJSONよりシリアライズされたデータのサイズが小さく、速度も速いらしいといろんなサイトに書いてある。例えば、Stackoverflowのこの記事を見ると、Gobの方がJSONよりデータサイズも半分以下で、速度も倍以上速い。すごいね、もうJSONなんか使ってる場合やないね。
単体の場合はJSONの方が小さい!
ちょうどKafkaでデータをやり取りするためのコードを書きかけていたので、シリアライゼーションのところで使ってみようと思い、Gobを試してみると、なぜかデータサイズがJSONより小さくならない、ていうか大きくなる。例えば次のようなコードを実行すると、JSONが24バイト、Gobが50バイトとJSONの方が小さくなる!?
package main
import (
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"github.com/tada3/gob-test2/domain"
)
func main() {
p := domain.Person{
Name: "Taro",
Id: 123,
}
checkSize(p)
}
func checkSize(p domain.Person) {
b, err := serializeJSON(p)
if err != nil {
panic(err)
}
fmt.Println("JSON:", len(b))
b1, err := serializeGob(p)
if err != nil {
panic(err)
}
fmt.Println("Gob:", len(b1))
}
func serializeJSON(p domain.Person) ([]byte, error) {
b, err := json.Marshal(p)
if err != nil {
return nil, err
}
return b, nil
}
func serializeGob(p domain.Person) ([]byte, error) {
buf := &bytes.Buffer{}
enc := gob.NewEncoder(buf)
err := enc.Encode(p)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
package domain
type Person struct {
Name string
Id int
}
結果はこの通り。
JSON: 24
Gob: 50
スライスなら勝てる?
おかしいな、と思いながら上のStackflowの記事のソースコードを見てみるとシリアライズの対象がstructのスライスであることに気づく。条件を同じにするために、上のテストコードを変更し、要素10のスライスをシリアライズするようにすると、JSONが241バイト、Gobが154バイトと逆転!
package main
import (
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"strconv"
"github.com/tada3/gob-test2/domain"
)
func main() {
ps := make([]domain.Person, 10)
for i := 0; i < 10; i++ {
ps[i].Name = "Taro" + strconv.Itoa(i)
ps[i].Id = i
}
checkSize(ps)
}
func checkSize(ps []domain.Person) {
// 単体の場合と同様
}
func serializeJSON(ps []domain.Person) ([]byte, error) {
// 単体の場合と同様
}
func serializeGob(ps []domain.Person) ([]byte, error) {
// 単体の場合と同様
}
JSON: 241
Gob: 154
型情報のサイズ
どうやら、同じstructを繰り返しシリアライズすると効率が良くなるように見える。今一度GobのGoDocを良く見てみるとEncode()のところに
Encode transmits the data item represented by the empty interface value, guaranteeing that all necessary type
information has been transmitted first.
と書いてある。Encode()は初回はデータ自体のシリアライズ結果だけではなく、型情報もシリアライズデータ(シリアライズされたデータ)に含める必要があるということだ。つまり、Gobの方がJSONより小さいというのは型情報を含まない、データ本体の部分に関しては常に正しいが、型情報のサイズも含めてしまうと成り立たないこともある。(一個目の例がそう。) 一方、型情報が必要なのは初回だけなので、繰り返し同じデータをシリアライズすると型情報のコストが薄まっていき、一定数を超えるとJSONよりも小さくなる。
結論
で、結局サイズだけで考えた場合にGobとJSONとどっちが良いのか?答えはEncoder/Decoderの使い方によると思われる。型情報のコストをなくすには、
- 同一のEncoder/Decoderのインスタンスのペアが、
- 一定数のパターンのスキーマに従うデータ構造を繰り返しシリアライズ/でシリアライズする
ということが必要となる。通常のアプリケーションだと扱うデータ構造のパターンは決まっているので、ほとんどの場合2は成り立つと思われるので1が成り立つがどうかが問題となる。クライアントアプリとサーバアプリでセッションを張ってデータをやりとりするような場合だと問題ないが、Webアプリケーションの場合は普通に作るとリクエストごとにEncoder/Decoderのインスタンスを作成するような形になってしまう。こういうケースは素直にJSONを使えば良いと思う。が、とにかくGobでデータサイズを減らしたい場合は、排他制御に気をつけつつ単一のEncoder/Decoderを使い回すか、コネクションプールみたいなやり方で一定数のEncoder/Decoderインスタンスを再利用する等の工夫が必要になってくる。
Discussion