這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
我在github上建立了一個Go語言序列化/還原序列化庫的效能比較的項目gosercomp,用來比較常見的Go語言生態圈的序列化庫。
效能是以Go官方庫提供的JSON/XML序列化庫為基準,比較一下第三庫能帶來多大的效能提升。
儘管一些第三方庫會自動產生Struct的代碼,我們還是都以下面的資料結構為例:
12345 |
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/youtube/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:
1234567 |
namespace go gosercompstruct ThriftColorGroup { 1: i32 id = 0, 2: string name, 3: list<string> colors,} |
比如flatbuffers:
123456789 |
namespace gosercomp;table FlatBufferColorGroup { cgId:int (id: 0); name:string (id: 1); colors: [string] (id: 2);}root_type FlatBufferColorGroup; |
對於protobuf:
12345678 |
package gosercomp;message ProtoColorGroup { required int32 id = 1; required string name = 2; repeated string colors = 3;} |
看以看出,所有測試的資料結構都是一致的,它包含三個欄位,一個是int類型的欄位Id,一個是string類型的欄位Name,一個是[]string類型的欄位Colors。
測試結果
完整的測試結果可以看這裡,
以下是Json、Xml、Protobuf、MessagePack、gencode的效能資料:
12345678910111213141516 |
benchmark _name iter time/iter alloc bytes/iter allocs/iter-------------------------------------------------------------------------------------------------------------------------BenchmarkMarshalByJson-4 1000000 1909 ns/op 376 B/op 4 allocs/opBenchmarkUnmarshalByJson-4 500000 4044 ns/op 296 B/op 9 allocs/opBenchmarkMarshalByXml-4 200000 7893 ns/op 4801 B/op 12 allocs/opBenchmarkUnmarshalByXml-4 100000 25615 ns/op 2807 B/op 67 allocs/opBenchmarkMarshalByProtoBuf-4 2000000 969 ns/op 328 B/op 5 allocs/opBenchmarkUnmarshalByProtoBuf-4 1000000 1617 ns/op 400 B/op 11 allocs/opBenchmarkMarshalByMsgp-4 5000000 256 ns/op 80 B/op 1 allocs/opBenchmarkUnmarshalByMsgp-4 3000000 459 ns/op 32 B/op 5 allocs/opBenchmarkMarshalByGencode-4 20000000 66.4 ns/op 0 B/op 0 allocs/opBenchmarkUnmarshalByGencode-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:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051 |
package gosercomp// NOTE: THIS FILE WAS PRODUCED BY THE// MSGP CODE GENERATION TOOL (github.com/tinylib/msgp)// DO NOT EDITimport ("github.com/tinylib/msgp/msgp")// MarshalMsg implements msgp.Marshalerfunc (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.Unmarshalerfunc (z *ColorGroup) UnmarshalMsg(bts []byte) (o []byte, err error) {var field []byte_ = fieldvar isz uint32isz, 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中,因此序列化後的資料包含對象的中繼資料。
還原序列化的時候會讀取欄位的名字,再將相應的位元組還原序列化賦值給對象的相應的欄位。
總體來說,MessagePack的效能已經相當高了,而且產生的資料也非常小,又是跨語言支援的,是值得關注的一個序列化庫。
gencode
對於MessagePack還有沒有可提升的空間?測試資料顯示, andyleap/gencode的效能還要好,甚至於效能是MessagePack的兩倍。
andyleap/gencode的目標也是提供快速而且資料很少的序列化庫。
它定義了自有的資料格式,並提供工具產生Golang代碼。
下面是我測試用的資料格式。
12345 |
struct GencodeColorGroup {Id int32Name stringColors []string} |
它提供了類似於Golang的資料類型struct,定義結構也類似, 並提供了一組資料類型。
你可以通過它的工具產生資料結構的代碼:
1 |
gencode.exe go -schema=gencode.schema -package gosercomp |
和MessagePack一樣的處理,對於大於或者等於0x80的整數,它會使用2個或者更多的位元組來表示。
但是與MessagePack不同的是,它不會寫入欄位的名字,也就是它不包含對象的中繼資料。同時,它寫入的額外資料只包含欄位的長度,並不需要指明資料的類型。
所有的值都以它的長度做先導,並沒有像MessagePack那樣為了節省空間的會對對象進行壓縮處理,所以它的代碼會更直接而有效。
當然它們的處理都是通過位元組的移位或者copy對字串直接進行拷貝,這樣的處理也非常的高效。
還原序列化的時候也是依次解析出各欄位的值,因為在編譯的時候已經知道每個欄位的類型,所以gencode無需中繼資料,可以聰明的對位元組按照流的方式順序處理。
可以看出,gencode相對於MessagePack,本身並沒有為資料中加入額外的中繼資料的資訊,也無需寫入欄位的類型資訊,這樣也可以減少產生的資料大小,同時它不會對小整數、短字串,小的Map進行刻意的壓縮,減少的代碼的複雜度和判斷分支,代碼更加的簡練而高效。
值得注意的是,gencode產生的程式碼除了官方庫外不依賴其它的第三方庫。
從測試資料來看,它的效能更勝一籌。