Why Protocol Buffer So Fast? ----protobuf編碼詳解

prtotocol buffer是google於2008年開源的一款非常優秀的序列化反序列化工具,它最突出的特點是輕便簡介,而且有很多語言的接口(官方的支持C++,Java,Python,C,以及第三方的Erlang, Perl等)。本文從protobuf如何將特定結構體序列化爲二進制流的角度,看看爲什麼Protobuf如此之快。

一,示例

從例子入手是學習一門新工具的最佳方法。下面我們通過一個簡單的例子看看我們如何用protobuf的C++接口序列化反序列化一個結構體。

1,編輯您將要序列化的結構體描述文件Hello.proto

image

每個結構體必須用message來描述,其中的每個字段的修飾符有required, repeated和optional三種,required表示該字段是必須的,repeated表示該字段可以重複出現,它描述的字段可以看做C語言中的數組,optional表示該字段可有可無。

同是,必須認爲地位每個字段賦予一個標號field_number,如上圖中的1,2,3,4所示。更詳細的proto文件的編寫規則見這裏

2,用protoc工具“編譯”Hello.proto

protoc工具使用的一般格式是:

protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto

其中SRC_DIR是proto文件所在的目錄,DST_DIR是編譯proto文件後生成的結構體處理文件的目錄

image

之後會生成對結構體Hello.proto中描述的各字段做序列化反序列化的類

image

3, 編寫序列化進程write.cc

image

我們用set方法爲結構體中的每個成員賦值,然後調用SerializeToOstream將結構體序列化到文件log中。

並編譯它:

image

4,編寫反序列化進程reader.cc

image

用ParseFromIstream將文件中的內容序列化到類Hello的對象msg中。

並編譯它:

image,

5,做序列化和反序列化操作

image

上面只是一個簡單的例子,並沒有對protobuf的性能做測試,protobuf的性能測試詳見這裏

二,protocol buffer的數據類型

從第一節中的例子可以看出,用Protocol buffer時需要用戶自定義自己的結構體,而且結構體中的定義規則要符合google制定的規則。結構體中每個字段都需要一個數據類型,protocol buffer支持的數據類型在源代碼wire_format_lite.h中定義:

image

其中:

VARINT類數據表示要用variant編碼對所傳入的數據做壓縮存儲,variant編碼細節見下一節。

FIXED32和FIXED64類數據不對用戶傳入的數據做variant壓縮存儲,只存儲原始數據。

LENGTH_DELIMITED類數據主要針對string類型、repeated類型和嵌套類型,對這些類型編碼時需要存儲他們的長度信息。

START_GROUP是一個組(該組可以是嵌套類型,也可以是repeated類型)的開始標誌。

END_GROUP是一個組(該組可以是嵌套類型,也可以是repeated類型)的結束標誌。

每類數據包含的具體數據類型如下表所示:

WireType                                                         表示類型

VARINT                                                 int32,int64,uint32,uint64,sint32,sint64,bool,enum

FIXED64                                                fixed64,sfixed64,double

LENGTH_DELIMITED                 string,bytes,embedded messages, packed repeadted field

START_GROUP                               group的開始標誌

END_GROUP                                     group的結束標誌

FIXED32                                               fixed32,sfixed32,float

三,protocol buffer的編碼

一言以蔽之,ProtocolBuffer的編碼是盡其所能地將字段的元信息和字段的值壓縮存儲,並且字段的元信息中含有對這個字段描述的所有信息。

整個結構體序列化後抽象地看起來像下圖這樣:

image

可以看到,整個消息是以二進制流的方式存儲,在這個二進制流中,逐個字段以定義的順序緊緊相鄰。每個字段中由元信息tag和字段的值value組成。

其中tag是這樣編碼的:

1)field_number << 3 | wire_type

2)對上面得到的無符號類型整數做variant編碼

其中field_number第一節中提到的每個字段的標號,wire_type是第二節中提到的該字段的數據類型。

1,variant編碼

variant是一種緊湊型數字編碼,將元數據跟數字保存在一起,如下圖所示是數字131415的variant編碼:

image

其中第一個字節的高位msb(Most Significant Bit )爲1表示下一個字節還有有效數據,msb爲0表示該字節中的後7爲是最後一組有效數字。踢掉最高位後的有效位組成真正的數字。

從上面可以看出,variant編碼存儲比較小的整數時很節省空間,小於等於127的數字可以用一個字節存儲。但缺點是對於大於

268,435,455(0xfffffff)的整數需要5個字節來存儲。但是一般情況下(尤其在tag編碼中)不會存儲這麼大的整數。

對一個整數的variant編碼的代碼位於

./src/google/protobuf/io/coded_stream.cc:WriteVarint32FallbackToArrayInline()函數中,摘錄如下;

inline uint8* CodedOutputStream::WriteVarint32FallbackToArrayInline(
    uint32 value, uint8* target) {
  target[0] = static_cast<uint8>(value | 0x80);
  if (value >= (1 << 7)) {
    target[1] = static_cast<uint8>((value >>  7) | 0x80);
    if (value >= (1 << 14)) {
      target[2] = static_cast<uint8>((value >> 14) | 0x80);
      if (value >= (1 << 21)) {
        target[3] = static_cast<uint8>((value >> 21) | 0x80);
        if (value >= (1 << 28)) {
          target[4] = static_cast<uint8>(value >> 28);
          return target + 5;
        } else {
          target[3] &= 0x7F;
          return target + 4;
        }
      } else {
        target[2] &= 0x7F;
        return target + 3;
      }
    } else {
      target[1] &= 0x7F;
      return target + 2;
    }
  } else {
    target[0] &= 0x7F;
    return target + 1;
  }
}

整個結構體的序列化過程如下:

a, 調用Hello類的ByteSize()計算出序列化後的長度,分配該長度的空間,以備以後將每個字段填充到該空間中,示例中的長度計算公式是:

                     1+Int32Size()+1+4+1+StringSize()

b, 調用Hello類的SerializeWithCachedSizes()對每個元素序列化

下面是對每一類元素的序列化編碼詳解

2 int32/int64/uint32/uint64類型的編碼

a,計算長度    1 + Int32Size(值);

b,調用WireFormatLite::WriteInt32(…)將該字段的元信息和字段值寫入到新空間中:

image

例如用戶爲int32傳入值123,則該字段的存儲如下:

第一個字節variant(1<<3|0)  第二個字節variant(123)

3,String類型的編碼

a, 計算長度  1 + variant(stringLength)+stringLength

b, 調用WireFormatLite::WriteString(…)將該字段的元信息、長度和值寫入到新空間中

image

例如用戶爲string傳入值“hello”,則該字段的存儲如下:

第一個字節variant(2<<3|2) ,第二個字節variant(5) ,剩餘的字節 “hello”

4,float類型的編碼

a, 計算長度 1+4

b,調用WireFormatLite::WriteFloat(…)將該字段的元信息和值寫入到新空間中

image

其中寫float內存拷貝的代碼非常精煉:

5, 嵌套結構體 編碼

a, 1 + variant32(embedded長度)+embedded的長度

b,調用WireFormatLite::WriteMessageMaybeToArray(…)將該字段的元信息、長度和值寫入到新空間中

image

6,repeated類型字段編碼

a,計算長度    1*repeated個數 + variant32(repeated長度)+repeated長度

b,調用WireFormatLite::WriteMessageMaybeToArray(…)將下圖所示編碼的值寫入到新空間中

image

7,sint32, sint64類型字段編碼

從int32編碼中可以看出,當int32傳入-1時所耗的空間很大,所以結構體定義中引入了sint32和sint64類型的數據,這種數據採用一種叫zigzag的編碼方式,使絕對值比較小的整數也佔用比較小的字節。

zigzag編碼的映射關係圖如下

image

它將原始類型爲int32的數用uint32的數表示,當一個數的絕對值比較小時,將其用uint32表示,再採用variant編碼存儲就會比較節省空間。

對一個整數的zigzag編碼也很巧妙:

四 總結

從上面的編碼可以看出, protocol buffer壓榨每一個沒有真正用到的字節,使之序列化後的字節儘量少,清晰的數據編碼和諸多的位操作使之變得很輕便簡潔高效。同時它提供了很多編程語言的接口,可以廣泛應用於RPC系統中。

但是,由於它將元信息編碼到二進制位中,使得序列化後的數據可讀性非常差(其實是沒有可讀性 ^.^)。

五 參考文獻

https://developers.google.com/protocol-buffers/ protobuf官方首頁

https://developers.google.com/protocol-buffers/docs/encoding 詳細講述了protobuf的編碼細節(有些地方比本文還詳細)

http://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking protobuf性能

http://code.google.com/p/protobuf/wiki/ThirdPartyAddOns提供了其他衆多語言實現的protocol buffer,但是安全性和效率不能保證

http://www.cppblog.com/colorful/archive/2012/05/05/173761.html提供了安裝protobuf的方法,並給出了一個小例子


轉載之

http://www.cnblogs.com/cobbliu/archive/2013/03/02/2940074.html


發佈了26 篇原創文章 · 獲贊 47 · 訪問量 59萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章