Protocol Buffers,FlatBuffersをGoとUnityで検証

gopherizeme

はじめに

既存の自社アプリのAPIサーバーのレスポンスデータのフォーマットはJSONを使っておりますが
クライアント側のデシリアライズ処理がかなりコストが高いので
今後、新規アプリで使うデシリアライザーを検証してみました。

今回の検証対象はFlatBuffers&Protocol Buffersです。
説明は割愛して検証結果のみ共有します。

Go(Server)側の検証

###検証方法
* GoのtestingパッケージのBenchmark機能を利用して計測します。
* json,protocolbuffer,flatbufferでネスト有り、無しでサイズ1000の配列を用意してランダムでMarshalとUnmarshalを行いました。
*
###結果
テスト項目、処理回数、平均処理時間、平均処理メモリ、Alloc回数

//benchresult
testing: warning: no tests to run
testing: warning: no tests to run
BenchmarkJsonMarshal-4 500000 2554 ns/op 696 B/op 4 allocs/op
BenchmarkJsonUnmarshal-4 300000 5761 ns/op 640 B/op 12 allocs/op
BenchmarkJsonMarshalNest-4 200000 9447 ns/op 2536 B/op 6 allocs/op
BenchmarkJsonUnmarshalNest-4 100000 22612 ns/op 1856 B/op 44 allocs/op
BenchmarkProtobufMarshal-4 2000000 712 ns/op 568 B/op 6 allocs/op
BenchmarkProtobufUnmarshal-4 2000000 776 ns/op 501 B/op 8 allocs/op
BenchmarkProtobufMarshalNest-4 500000 2548 ns/op 2248 B/op 9 allocs/op
BenchmarkProtobufUnmarshalNest-4 500000 2622 ns/op 1349 B/op 28 allocs/op
BenchmarkFlatbufMarshal-4 1000000 1199 ns/op 872 B/op 14 allocs/op
BenchmarkFlatbufUnmarshal-4 3000000 436 ns/op 165 B/op 6 allocs/op
BenchmarkFlatbufMarshalNest-4 500000 3545 ns/op 3224 B/op 19 allocs/op
BenchmarkFlatbufUnmarshalNest-4 500000 2426 ns/op 1077 B/op 25 allocs/op
PASS
ok serialization_benchmarks 21.031s

ネストがない場合protobuffesも早いですがFlatbufferのUnmarshalは爆速ですね。。Jsonの13倍弱。。すごい!
しかしネストが深くなるとprobuffersとの差があまりないのがわかりました。またMarshalはprobuffers方が早い!
検証に使用したコードはこちら

###コード

// bench_test.go
package bench

import (
"encoding/json"
"fmt"
"math/rand"
"testing"
"time"

"serialization_benchmarks/fb"

"github.com/golang/protobuf/proto"
flatbuffers "github.com/google/flatbuffers/go"
)

type Serializer interface {
Marshal(o interface{}) []byte
Unmarshal(d []byte, o interface{}) error
String() string
}

func randString(l int) string {
buf := make([]byte, l)
for i := 0; i < (l+1)/2; i++ {
buf[i] = byte(rand.Intn(256))
}
return fmt.Sprintf("%x", buf)[:l]
}

func generate() []*UserResponse {
a := make([]*UserResponse, 0, 1000)
t := fmt.Sprint(time.Now().Format("2006-01-02 15:04:05"))
for i := 0; i < 1000; i++ {
a = append(a, &UserResponse{
Uid: time.Now().UnixNano(),
Muid: randString(32),
Status: randString(5),
Tutorial: time.Now().UnixNano(),
Nickname: randString(20),
TotalLoginNum: time.Now().UnixNano(),
OpenedAt: t,
CreatedAt: t,
UpdatedAt: t,
})
}
return a
}

func benchMarshal(b *testing.B, s Serializer) {
b.StopTimer()
data := generate()
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
s.Marshal(data[rand.Intn(len(data))])
}
}

func benchUnmarshal(b *testing.B, s Serializer) {
b.StopTimer()
data := generate()
ser := make([][]byte, len(data))
for i, d := range data {
o := s.Marshal(d)
t := make([]byte, len(o))
copy(t, o)
ser[i] = t
}
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
n := rand.Intn(len(ser))
o := &UserResponse{}
err := s.Unmarshal(ser[n], o)
if err != nil {
b.Fatalf("%s failed to unmarshal: %s (%s)", s, err, ser[n])
}
}
}

func benchMarshalNest(b *testing.B, s Serializer) {
b.StopTimer()
data := generateNest()
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
s.Marshal(data[rand.Intn(len(data))])
}
}

func benchUnmarshalNest(b *testing.B, s Serializer) {
b.StopTimer()
data := generateNest()
ser := make([][]byte, len(data))
for i, d := range data {
o := s.Marshal(d)
t := make([]byte, len(o))
copy(t, o)
ser[i] = t
}
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
n := rand.Intn(len(ser))
o := &All{}
err := s.Unmarshal(ser[n], o)
if err != nil {
b.Fatalf("%s failed to unmarshal: %s (%s)", s, err, ser[n])
}
}
}

type JsonSerializer struct{}

func (j JsonSerializer) Marshal(o interface{}) []byte {
d, _ := json.Marshal(o)
return d
}

func (j JsonSerializer) Unmarshal(d []byte, o interface{}) error {
return json.Unmarshal(d, o)
}

func (j JsonSerializer) String() string {
return "json"
}

func BenchmarkJsonMarshal(b *testing.B) {
benchMarshal(b, JsonSerializer{})
}

func BenchmarkJsonUnmarshal(b *testing.B) {
benchUnmarshal(b, JsonSerializer{})
}

func BenchmarkJsonMarshalNest(b *testing.B) {
benchMarshalNest(b, JsonSerializer{})
}

func BenchmarkJsonUnmarshalNest(b *testing.B) {
benchUnmarshalNest(b, JsonSerializer{})
}

// protobuf
func BenchmarkProtobufMarshal(b *testing.B) {
b.StopTimer()
data := generate()
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
proto.Marshal(data[rand.Intn(len(data))])
}
}

func BenchmarkProtobufUnmarshal(b *testing.B) {
b.StopTimer()
data := generate()
ser := make([][]byte, len(data))
for i, d := range data {
ser[i], _ = proto.Marshal(d)
}
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
n := rand.Intn(len(ser))
o := &UserResponse{}
err := proto.Unmarshal(ser[n], o)
if err != nil {
b.Fatalf("goprotobuf failed to unmarshal: %s (%s)", err, ser[n])
}
}
}
func generateNest() []*All {
t := fmt.Sprint(time.Now().Format("2006-01-02 15:04:05"))
a := make([]*All, 0, 1000)
for i := 0; i < 1000; i++ {
a = append(a, &All{
Uid: time.Now().UnixNano(),
Usera: &Usera{
Uid: time.Now().UnixNano(),
Muid: randString(32),
Status: randString(5),
Tutorial: time.Now().UnixNano(),
Nickname: randString(20),
TotalLoginNum: time.Now().UnixNano(),
OpenedAt: t,
CreatedAt: t,
UpdatedAt: t,
},
Userb: &Userb{
Uid: time.Now().UnixNano(),
Muid: randString(32),
Status: randString(5),
Tutorial: time.Now().UnixNano(),
Nickname: randString(20),
TotalLoginNum: time.Now().UnixNano(),
OpenedAt: t,
CreatedAt: t,
UpdatedAt: t,
Userc: &Userc{
Uid: time.Now().UnixNano(),
Muid: randString(32),
Status: randString(5),
Tutorial: time.Now().UnixNano(),
Nickname: randString(20),
TotalLoginNum: time.Now().UnixNano(),
OpenedAt: t,
CreatedAt: t,
Userd: &Userd{
Uid: time.Now().UnixNano(),
Muid: randString(32),
Status: randString(5),
Tutorial: time.Now().UnixNano(),
Nickname: randString(20),
TotalLoginNum: time.Now().UnixNano(),
OpenedAt: t,
CreatedAt: t,
},
},
},
})
}
return a
}

func BenchmarkProtobufMarshalNest(b *testing.B) {
b.StopTimer()
data := generateNest()
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
proto.Marshal(data[rand.Intn(len(data))])
}
}

func BenchmarkProtobufUnmarshalNest(b *testing.B) {
b.StopTimer()
data := generateNest()
ser := make([][]byte, len(data))
for i, d := range data {
ser[i], _ = proto.Marshal(d)
}
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
n := rand.Intn(len(ser))
o := &All{}
err := proto.Unmarshal(ser[n], o)
if err != nil {
b.Fatalf("goprotobuf failed to unmarshal: %s (%s)", err, ser[n])
}
}
}

// FlatBuffers

func fbMarshal(obj *UserResponse) []byte {
builder := flatbuffers.NewBuilder(0)
muID := builder.CreateString(obj.Muid)
status := builder.CreateString(obj.Status)
nickname := builder.CreateString(obj.Nickname)
openedAt := builder.CreateString(obj.OpenedAt)
createdAt := builder.CreateString(obj.CreatedAt)
updatedAt := builder.CreateString(obj.UpdatedAt)
fb.UseraStart(builder)
fb.UseraAddUid(builder, obj.Uid)
fb.UseraAddMuid(builder, muID)
fb.UseraAddStatus(builder, status)
fb.UseraAddTutorial(builder, obj.Tutorial)
fb.UseraAddNickname(builder, nickname)
fb.UseraAddTotalLoginNum(builder, obj.TotalLoginNum)
fb.UseraAddOpenedAt(builder, openedAt)
fb.UseraAddCreatedAt(builder, createdAt)
fb.UseraAddUpdatedAt(builder, updatedAt)
end := fb.UseraEnd(builder)
builder.Finish(end)

return builder.Bytes[builder.Head():]
}

func fbUnmarshal(data []byte) UserResponse {
obj := fb.GetRootAsUsera(data, 0)
userResponse := UserResponse{
Uid: obj.Uid(),
Muid: string(obj.Muid()),
Status: string(obj.Status()),
Tutorial: obj.Tutorial(),
Nickname: string(obj.Nickname()),
TotalLoginNum: obj.TotalLoginNum(),
OpenedAt: string(obj.OpenedAt()),
CreatedAt: string(obj.CreatedAt()),
UpdatedAt: string(obj.UpdatedAt()),
}
return userResponse
}

func BenchmarkFlatbufMarshal(b *testing.B) {
b.StopTimer()
data := generate()
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
fbMarshal(data[rand.Intn(len(data))])
}

}

func BenchmarkFlatbufUnmarshal(b *testing.B) {
b.StopTimer()
data := generate()
ser := make([][]byte, len(data))
for i, d := range data {
ser[i] = fbMarshal(d)
}
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
_ = fbUnmarshal(ser[rand.Intn(len(ser))])
}
}

func fbMarshalNest(obj *All) []byte {
b := flatbuffers.NewBuilder(0)
muID := b.CreateString(obj.Usera.Muid)
status := b.CreateString(obj.Usera.Status)
nickname := b.CreateString(obj.Usera.Nickname)
t1 := b.CreateString(obj.Usera.OpenedAt)
t2 := b.CreateString(obj.Usera.CreatedAt)
t3 := b.CreateString(obj.Usera.UpdatedAt)

fb.UseraStart(b)
fb.UseraAddUid(b, obj.Usera.Uid)
fb.UseraAddMuid(b, muID)
fb.UseraAddStatus(b, status)
fb.UseraAddTutorial(b, obj.Usera.Tutorial)
fb.UseraAddNickname(b, nickname)
fb.UseraAddTotalLoginNum(b, obj.Usera.TotalLoginNum)
fb.UseraAddOpenedAt(b, t1)
fb.UseraAddCreatedAt(b, t2)
fb.UseraAddUpdatedAt(b, t3)
usera := fb.UseraEnd(b)

muID = b.CreateString(obj.Userb.Userc.Userd.Muid)
status = b.CreateString(obj.Userb.Userc.Userd.Status)
nickname = b.CreateString(obj.Userb.Userc.Userd.Nickname)
t1 = b.CreateString(obj.Userb.Userc.Userd.OpenedAt)
t2 = b.CreateString(obj.Userb.Userc.Userd.CreatedAt)
t3 = b.CreateString(obj.Userb.Userc.Userd.UpdatedAt)
fb.UserdStart(b)
fb.UserdAddUid(b, obj.Userb.Userc.Userd.Uid)
fb.UserdAddMuid(b, muID)
fb.UserdAddStatus(b, status)
fb.UserdAddTutorial(b, obj.Userb.Userc.Userd.Tutorial)
fb.UserdAddNickname(b, nickname)
fb.UserdAddTotalLoginNum(b, obj.Userb.Userc.Userd.TotalLoginNum)
fb.UserdAddOpenedAt(b, t1)
fb.UserdAddCreatedAt(b, t2)
fb.UserdAddUpdatedAt(b, t3)
userd := fb.UserdEnd(b)

muID = b.CreateString(obj.Userb.Userc.Muid)
status = b.CreateString(obj.Userb.Userc.Status)
nickname = b.CreateString(obj.Userb.Userc.Nickname)
t1 = b.CreateString(obj.Userb.Userc.OpenedAt)
t2 = b.CreateString(obj.Userb.Userc.CreatedAt)
t3 = b.CreateString(obj.Userb.Userc.UpdatedAt)

fb.UsercStart(b)
fb.UsercAddUid(b, obj.Userb.Userc.Uid)
fb.UsercAddMuid(b, muID)
fb.UsercAddStatus(b, status)
fb.UsercAddTutorial(b, obj.Userb.Userc.Tutorial)
fb.UsercAddNickname(b, nickname)
fb.UsercAddTotalLoginNum(b, obj.Userb.Userc.TotalLoginNum)
fb.UsercAddOpenedAt(b, t1)
fb.UsercAddCreatedAt(b, t2)
fb.UsercAddUpdatedAt(b, t3)
fb.UsercAddUserd(b, userd)
userc := fb.UsercEnd(b)

muID = b.CreateString(obj.Userb.Muid)
status = b.CreateString(obj.Userb.Status)
nickname = b.CreateString(obj.Userb.Nickname)
t1 = b.CreateString(obj.Userb.OpenedAt)
t2 = b.CreateString(obj.Userb.CreatedAt)
t3 = b.CreateString(obj.Userb.UpdatedAt)
fb.UserbStart(b)
fb.UserbAddUid(b, obj.Userb.Uid)
fb.UserbAddMuid(b, muID)
fb.UserbAddStatus(b, status)
fb.UserbAddTutorial(b, obj.Userb.Tutorial)
fb.UserbAddNickname(b, nickname)
fb.UserbAddTotalLoginNum(b, obj.Userb.TotalLoginNum)
fb.UserbAddOpenedAt(b, t1)
fb.UserbAddCreatedAt(b, t2)
fb.UserbAddUpdatedAt(b, t3)
fb.UserbAddUserc(b, userc)
userb := fb.UserbEnd(b)

uid := time.Now().UnixNano()
fb.AllStart(b)
fb.AllAddUid(b, uid)
fb.AllAddUsera(b, usera)
fb.AllAddUserb(b, userb)
all := fb.AllEnd(b)
b.Finish(all)
return b.Bytes[b.Head():]
}

func fbUnmarshalNest(data []byte) All {
obj := fb.GetRootAsAll(data, 0)
usera := fb.GetRootAsUsera(data, 0)
userb := fb.GetRootAsUserb(data, 0)
userc := fb.GetRootAsUserc(data, 0)
userd := fb.GetRootAsUserd(data, 0)
return All{
Uid: obj.Uid(),
Usera: &Usera{
Uid: obj.Usera(usera).Uid(),
Muid: string(obj.Usera(usera).Muid()),
Status: string(obj.Usera(usera).Status()),
Tutorial: obj.Usera(usera).Tutorial(),
Nickname: string(obj.Usera(usera).Nickname()),
TotalLoginNum: obj.Usera(usera).TotalLoginNum(),
OpenedAt: string(obj.Usera(usera).OpenedAt()),
CreatedAt: string(obj.Usera(usera).CreatedAt()),
UpdatedAt: string(obj.Usera(usera).UpdatedAt()),
},
Userb: &Userb{
Uid: obj.Userb(userb).Uid(),
Muid: string(obj.Userb(userb).Muid()),
Status: string(obj.Userb(userb).Status()),
Tutorial: obj.Userb(userb).Tutorial(),
Nickname: string(obj.Userb(userb).Nickname()),
TotalLoginNum: obj.Userb(userb).TotalLoginNum(),
OpenedAt: string(obj.Userb(userb).OpenedAt()),
CreatedAt: string(obj.Userb(userb).CreatedAt()),
UpdatedAt: string(obj.Userb(userb).UpdatedAt()),
Userc: &Userc{
Uid: obj.Userb(userb).Userc(userc).Uid(),
Muid: string(obj.Userb(userb).Userc(userc).Muid()),
Status: string(obj.Userb(userb).Userc(userc).Status()),
Tutorial: obj.Userb(userb).Userc(userc).Tutorial(),
Nickname: string(obj.Userb(userb).Userc(userc).Nickname()),
TotalLoginNum: obj.Userb(userb).Userc(userc).TotalLoginNum(),
OpenedAt: string(obj.Userb(userb).Userc(userc).OpenedAt()),
CreatedAt: string(obj.Userb(userb).Userc(userc).CreatedAt()),
UpdatedAt: string(obj.Userb(userb).Userc(userc).UpdatedAt()),
Userd: &Userd{
Status: string(obj.Userb(userb).Userc(userc).Userd(userd).Status()),
Tutorial: obj.Userb(userb).Userc(userc).Userd(userd).Tutorial(),
Nickname: string(obj.Userb(userb).Userc(userc).Userd(userd).Nickname()),
TotalLoginNum: obj.Userb(userb).Userc(userc).Userd(userd).TotalLoginNum(),
OpenedAt: string(obj.Userb(userb).Userc(userc).Userd(userd).OpenedAt()),
CreatedAt: string(obj.Userb(userb).Userc(userc).Userd(userd).CreatedAt()),
UpdatedAt: string(obj.Userb(userb).Userc(userc).Userd(userd).UpdatedAt()),
},
},
},
}
}

func BenchmarkFlatbufMarshalNest(b *testing.B) {
b.StopTimer()
data := generateNest()
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
fbMarshalNest(data[rand.Intn(len(data))])
}
}

func BenchmarkFlatbufUnmarshalNest(b *testing.B) {
b.StopTimer()
data := generateNest()
ser := make([][]byte, len(data))
for i, d := range data {
ser[i] = fbMarshalNest(d)
}
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
_ = fbUnmarshalNest(ser[rand.Intn(len(ser))])
}
}

テストで使ったJSONフォーマット

//simple.json
{
"uid": 1478840380636088868,
"muid": "5be400a6ef8fa215480142fb11de5079",
"status": "f418d",
"tutorial": 1478840380636094689,
"nickname": "31d909984dd0ae97245c",
"total_login_num": 1478840380636095716,
"opened_at": "2016-11-11 13:59:40",
"created_at": "2016-11-11 13:59:40",
"updated_at": "2016-11-11 13:59:40"
}
//json:nested.json
{
"list": [{
"uid": 1478840380636088708,
"usera": {
"uid": 1478840380636088868,
"muid": "5be400a6ef8fa215480142fb11de5079",
"status": "f418d",
"tutorial": 1478840380636094689,
"nickname": "31d909984dd0ae97245c",
"total_login_num": 1478840380636095716,
"opened_at": "2016-11-11 13:59:40",
"created_at": "2016-11-11 13:59:40",
"updated_at": "2016-11-11 13:59:40"
},
"userb": {
"uid": 1478840380636095874,
"muid": "a9681ee28711a2b87c30a77085b69922",
"status": "c14f1",
"tutorial": 1478840380636097629,
"nickname": "66e6937c5a102dfe1c5b",
"total_login_num": 1478840380636098843,
"opened_at": "2016-11-11 13:59:40",
"created_at": "2016-11-11 13:59:40",
"updated_at": "2016-11-11 13:59:40",
"userc": {
"uid": 1478840380636099000,
"muid": "406f9df52f4dc22d4890b419399f625e",
"status": "e79af",
"tutorial": 1478840380636100833,
"nickname": "413cde90d67f5dc2e151",
"total_login_num": 1478840380636101956,
"opened_at": "2016-11-11 13:59:40",
"created_at": "2016-11-11 13:59:40",
"userd": {
"uid": 1478840380636102113,
"muid": "f170bf033c4e906a9fcb70a16bc37769",
"status": "05e70",
"tutorial": 1478840380636103838,
"nickname": "1e19aa99caecc1c039bf",
"total_login_num": 1478840380636104883,
"opened_at": "2016-11-11 13:59:40",
"created_at": "2016-11-11 13:59:40"
}
}
}
}]
}

Unty(Client)側の検証

検証方法

iOS,android端末からサーバーにリクエストしてデータ量、通信時間、デシリアライズ時間を計測する

検証結果

デシリアライズと通信時間 単位(ms)

項目 iOS android
DeserializeJson 25.79 134.03
DeserializeProto 21.45 94.23
DeserializeFlat 0.3 0.24
RqeustJson 215.19 739.25
RequestProto 158.11 288.08
RequestFlat 133.0948 256.833

データサイズ単位(byte)

タイプ サイズ
json 1166010
protobuffers 611000
flatbuffers 876076

protobuffersが一番小さかった

OS:iOS 機種:iPhone7 Plus

blog1

OS:android 機種:Xperia™ Z3 SO-01G

blog2

2b5a5907-cf0c-3a27-b8da-22f49780ebd6

おわりに

FlatBufferはDeserializeコストがほぼ0なのでクライアント側で使うには一番いいかと思いました。
しかしサーバー側だと主に使うのはserializeが多いと思われるし。。
シリアライズ後のデータサイズから見てもprobuffersが一番良いのではと思いました。
Goのベンチマークで書いたコードを見ればわかると思いますがFlatbuffersの書き方ははあまり行けてなく人間がわかりにくいので導入するのは躊躇しますね。。