背景
JSONよりもProtobufの方が
ということは色んな検証記事から明らかになっています。
一方でGoのProtobufはデータをstreamで扱うのではなく、全部メモリに載せてシリアライズ・デシリアライズするAPI(メソッド)しかありません。
過去にこんな記事を書いたように、streamで扱うことで大きくメモリ使用量が改善されるケースは多々あるので、
もしかして大きなデータをstructにデシリアライズする場合、メモリ使用量としてはJSONでstreamで扱う方が少なくなるのかな?と思って検証してみました。
環境
- Go 1.17.2
検証
検証方法
- ファイルの読み込み
- structへデシリアライズ
のフローを以下の4つのケースで検証してみました。
元のデータは同じですが、データ形式でかなり差がありますね。
ケース | データ形式 | ファイルサイズ |
---|---|---|
a | JSON | 86MB |
b | gzip圧縮したJSON | 9.3MB |
c | Protobuf | 52MB |
d | gzip圧縮したProtobuf | 8.5MB |
検証コード
a. JSON
JSONはstreamで扱います。
func ReadJSON(path string) (*LargeData, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() entity := &LargeData{} err = json.NewDecoder(f).Decode(entity) if err != nil { return nil, err } return entity, nil }
b. gzip圧縮したJSON
func ReadGZipJSON(path string) (*LargeData, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() gr, err := gzip.NewReader(f) if err != nil { return nil, err } defer gr.Close() entity := &LargeData{} err = json.NewDecoder(gr).Decode(entity) if err != nil { return nil, err } return entity, nil }
c. Protobuf
Protobufはstreamで扱えないので、os.ReadFileで一気にメモリに載せます。
func ReadProtobuf(path string) (*LargeData, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } entity := &LargeData{} err = entity.Unmarshal(data) if err != nil { return nil, err } return entity, nil }
d. gzip圧縮したProtobuf
gzip展開まではstreamで扱いますが、それ以降は先程と同じです。
func ReadGZipProtobuf(path string) (*LargeData, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() gr, err := gzip.NewReader(f) if err != nil { return nil, err } data := new(bytes.Buffer) _, err = io.Copy(data, gr) if err != nil { return nil, err } entity := &LargeData{} err = entity.Unmarshal(data.Bytes()) if err != nil { return nil, err } return entity, nil }
結果
$ go test -bench . -benchmem goos: darwin goarch: amd64 cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz BenchmarkReadJSON-16 1 1401583800 ns/op 586628144 B/op 2022214 allocs/op BenchmarkReadGZipJSON-16 1 1529638129 ns/op 587703008 B/op 2025822 allocs/op BenchmarkReadProtobuf-16 7 167106390 ns/op 419643998 B/op 1199804 allocs/op BenchmarkReadGZipProtobuf-16 4 333084560 ns/op 502478978 B/op 1215377 allocs/op
ケース | メモリ使用量 |
---|---|
a. JSON | 587MB |
b. gzip圧縮したJSON | 588MB |
c. Protobuf | 420MB |
d. gzip圧縮したProtobuf | 502MB |
仮説と異なり、Protobufの方が省メモリであることが分かりました。
また処理速度は断然Protobufの方が速いですね。
まとめ
ゲームのマスターデータなど静的かつ大きなデータをS3バケットからロードする際、プロファイリングでprotobufのUnmarshal()のメモリ使用量が高かったため今回の検証をしてみました。
仮説としてはstreamのJSONの方が省メモリで、そうであればProtobufよりもJSONで管理する方が良いかもと思っていましたが結果は逆でした。
Goの場合streamで扱うJSONよりもProtobufでまるっとメモリに載せた方がメモリ使用量は少ないことが分かりました。