Golang序列化框架對決 - 為什么andyleap/gencode那么快?
我在github上創建了一個Go語言序列化/反序列化庫的性能比較的項目gosercomp,用來比較常見的Go語言生態圈的序列化庫。
性能是以Go官方庫提供的JSON/XML序列化庫為基準,比較一下第三庫能帶來多大的性能提升。
盡管一些第三方庫會自動產生Struct的代碼,我們還是都以下面的數據結構為例:
type ColorGroup struct { Id int `json:"id" xml:"id,attr" msg:"id"` Name string `json:"name" xml:"name" msg:"name"` Colors [] string `json:"colors" xml:"colors" msg:"colors"` }
其中Colors是一個slice。我并沒有測試Struct嵌套以及循環引用的情況。
目前本項目包含了以下幾種序列化庫的性能比較:
- encoding/json
- encoding/xml
- github.com/油Tube/vitess/go/bson
- github.com/tinylib/msgp
- github.com/golang/protobuf
- github.com/gogo/protobuf
- github.com/google/flatbuffers
- Apache/Thrift
- Apache/Avro
- andyleap/gencode
- ugorji/go/codec
對于序列化庫的實現來講,如果在運行時通過反射的方式進行序列化和反序列化,性能不會太好,比如官方庫的Json和Xml序列化方法,所以高性能的序列化庫很多都是通過代碼生成在編譯的時候提供序列化和反序列化的方法,下面我會介紹MessagePack和gencode兩種性能較高的序列化庫。
本項目受alecthomas/go_serialization_benchmarks項目的啟發。
對于第三方的序列化庫,它們的數據結構的定義可能是自有形式的,比如Thrift:
比如flatbuffers:namespace go gosercompstruct ThriftColorGroup {1: i32 id = 0,2: string name,3: list<string> colors,}
namespace gosercomp; table FlatBufferColorGroup { cgId:int (id: 0); name:string (id: 1); colors: [string] (id: 2); } root_type FlatBufferColorGroup;
對于protobuf:
看以看出,所有測試的數據結構都是一致的,它包含三個字段,一個是int類型的字段Id,一個是string類型的字段Name,一個是[]string類型的字段Colors。package gosercomp;message ProtoColorGroup {required int32 id = 1;required string name = 2;repeated string colors = 3;}
測試結果
完整的測試結果可以看這里,
以下是Json、Xml、Protobuf、MessagePack、gencode的性能數據:
benchmark _name iter time/iter alloc bytes/iter allocs/iter ---------------------------------------------------------------------------------------BenchmarkMarshalByJson-4 1000000 1909 ns/op 376 B/op 4 allocs/op BenchmarkUnmarshalByJson-4 500000 4044 ns/op 296 B/op 9 allocs/op BenchmarkMarshalByXml-4 200000 7893 ns/op 4801 B/op 12 allocs/op BenchmarkUnmarshalByXml-4 100000 25615 ns/op 2807 B/op 67 allocs/op BenchmarkMarshalByProtoBuf-4 2000000 969 ns/op 328 B/op 5 allocs/op BenchmarkUnmarshalByProtoBuf-4 1000000 1617 ns/op 400 B/op 11 allocs/op BenchmarkMarshalByMsgp-4 5000000 256 ns/op 80 B/op 1 allocs/op BenchmarkUnmarshalByMsgp-4 3000000 459 ns/op 32 B/op 5 allocs/op BenchmarkMarshalByGencode-4 20000000 66.4 ns/op 0 B/op 0 allocs/op BenchmarkUnmarshalByGencode-4 5000000 271 ns/op 32 B/op 5 allocs/op
可以看出Json、Xml的序列化和反序列化性能是很差的。想比較而言MessagePack有10x的性能的提升,而gencode比MessagePack的性能還要好很多。
MessagePack的實現
MessagePack是一個高效的二進制序列化格式。它可以在多種語言直接交換數據格式。它將對象可以序列化更小的格式,比如對于很小的整數,它可以使用更少的存儲(一個字節)。對于短字符串,它只需一個額外的字節來指示。
上圖是一個27個字節的JSON數據,如果使用MessagePack的話可以用18個字節就可以表示了。
可以看出每個類型需要額外的0到n個字節來指明(數量依賴對象的大小或者長度)。上面的例子中82指示這個對象是包含兩個元素的map (0x80 + 2), A7 代表一個短長度的字符串,字符串長度是7。C3代表true,C2代表false,C0代表nil。00代表是一個小整數。
完整的格式可以參照官方規范。
MessagePack支持多種開發語言。
題外話,一個較新的RFC規范 CBOR/rfc7049 (簡潔的二進制對象表示)定義了一個類似的規范,可以表達更詳細的內容。
推薦使用的Go MessagePack庫是 tinylib/msgp,它比ugorji/go有更好的性能。
tinylib/msgp提供了一個代碼生成工具msgp,可以為Golang的Struct生成序列化的代碼,當然你的Struct應該定義msg標簽,如本文上面定義的ColorGroup。通過go generate就可以自動生成代碼,如本項目中生成的msgp_gen.go:
package gosercomp // NOTE: THIS FILE WAS PRODUCED BY THE // MSGP CODE GENERATION TOOL (github.com/tinylib/msgp) // DO NOT EDIT import ( "github.com/tinylib/msgp/msgp" ) // MarshalMsg implements msgp.Marshaler func (z *ColorGroup) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // map header, size 3 // string "id" o = append(o, 0x83, 0xa2, 0x69, 0x64) o = msgp.AppendInt(o, z.Id) // string "name" o = append(o, 0xa4, 0x6e, 0x61, 0x6d, 0x65) o = msgp.AppendString(o, z.Name) // string "colors" o = append(o, 0xa6, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73) o = msgp.AppendArrayHeader(o, uint32(len(z.Colors))) for xvk := range z.Colors { o = msgp.AppendString(o, z.Colors[xvk]) } return } // UnmarshalMsg implements msgp.Unmarshaler func (z *ColorGroup) UnmarshalMsg(bts []byte) (o []byte, err error) { var field []byte _ = field var isz uint32 isz, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { return } for isz > 0 { isz-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { return } switch msgp.UnsafeString(field) { case "id": z.Id, bts, err = msgp.ReadIntBytes(bts) if err != nil { return } case "name":生成的代碼的使用類似官方庫的Json和Xml,提供了Marshal和UnmarshalMsg的方法。
結合MessagePack的規范,可以看到MarshalMsg方法很簡潔的,它使用了msgp.AppendXXX方法將相應的類型的數據寫入到[]byte中,你可以預先分配/重用[]byte,這樣可以實現 zero alloc。同時你也注意到,它也將字段的名字寫入到序列化字節slice中,因此序列化后的數據包含對象的元數據。生成的代碼的使用類似官方庫的Json和Xml,提供了Marshal和UnmarshalMsg的方法。
反序列化的時候會讀取字段的名字,再將相應的字節反序列化賦值給對象的相應的字段。
總體來說,MessagePack的性能已經相當高了,而且生成的數據也非常小,又是跨語言支持的,是值得關注的一個序列化庫。
gencode
對于MessagePack還有沒有可提升的空間?測試數據顯示, andyleap/gencode的性能還要好,甚至于性能是MessagePack的兩倍。
andyleap/gencode的目標也是提供快速而且數據很少的序列化庫。
它定義了自有的數據格式,并提供工具生成Golang代碼。
下面是我測試用的數據格式。
struct GencodeColorGroup { Id int32 Name string Colors [] string }
它提供了類似于Golang的數據類型struct,定義結構也類似, 并提供了一組數據類型。
你可以通過它的工具生成數據結構的代碼:
gencode.exe go -schema=gencode.schema -package gosercomp
和MessagePack一樣的處理,對于大于或者等于0x80的整數,它會使用2個或者更多的字節來表示。
但是與MessagePack不同的是,它不會寫入字段的名字,也就是它不包含對象的元數據。同時,它寫入的額外數據只包含字段的長度,并不需要指明數據的類型。
所有的值都以它的長度做先導,并沒有像MessagePack那樣為了節省空間會對對象進行壓縮處理,所以它的代碼會更直接而有效。
當然它們的處理都是通過字節的移位或者copy對字符串直接進行拷貝,這樣的處理也非常的高效。
反序列化的時候也是依次解析出各字段的值,因為在編譯的時候已經知道每個字段的類型,所以gencode無需元數據,可以聰明的對字節按照流的方式順序處理。
可以看出,gencode相對于MessagePack,本身并沒有為數據中加入額外的元數據的信息,也無需寫入字段的類型信息,這樣也可以減少生成的數據大小,同時它不會對小整數、短字符串,小的Map進行刻意的壓縮,減少的代碼的復雜度和判斷分支,代碼更加的簡練而高效。
值得注意的是,gencode生成的代碼除了官方庫外不依賴其它的第三方庫。
從測試數據來看,它的性能更勝一籌。
來源:http://colobu.com/2016/03/16/why-is-go-gencode-so-fast/