一、為什么使用varint編碼
在常規的TLV(TAG Length Value)編碼格式中,我們注意到其中有一個必然存在的Length字段。這個就是管理的成本,也就是為了實現管理,管理結構本身也會帶來消耗。對int這種最為常見的類型來說,通常現實生活中的自然數范圍都比較小,所以定長的4個字節表示1個int32通常都是浪費的。例如整數表示大家的工資、一個班級的人數、學生的成績等。
假設說一個字節可以用一個字節來表示,例如百分制的成績,那么使用TLV要是加上一個1字節來表示長度就有些浪費了。所以此時可以和UTF-8編碼類似,就是使用字節的最高bit表示一個整數是否結束,這樣就相當於把長度信息化整為零編碼到字節流中。具體來說:如果最高bit為1,則表示后面還有額外的bit流。這樣可以看到,我們省掉了一個專門的表示長度的Length字段,而是把這個信息編碼到字節流的最高bit中了。
二、為什么使用packed
關於packed的說明。這里可以看到,其對於repeated的類型聲明為了大家最喜聞樂見的“長度+內容”的形式,這里的區別在於“內容”這個部分,按照標准的編碼方式,每個字段前面都是需要有TL編碼的,也就是例子中的 3, 270, and 86942 三個數值,每個數值都應該是20(其中的4表示d的tag,0表示為變長整數)。也就是20 03 20 8E 02 20 9E A7 05。注意每個真實存儲字段前都有個TAG(4<<3)+Type(0)的前綴。
但是在使用了pack之后,默認的存儲是把公共的20提到了整個存儲的前面,並且改變了類型,從20變成了22(從varint變換長了LEN),之后的存儲類型也變化了,后面引導的是一個表示后面總長度的字段。但是文檔中也明確說明了這個屬性只能對基礎結構使用(varint、int32、int64字段)。
Version 2.1.0 introduced packed repeated fields, which in proto2 are declared like repeated fields but with the special [packed=true] option. In proto3, repeated fields of scalar numeric types are packed by default. These function like repeated fields, but are encoded differently. A packed repeated field containing zero elements does not appear in the encoded message. Otherwise, all of the elements of the field are packed into a single key-value pair with wire type 2 (length-delimited). Each element is encoded the same way it would be normally, except without a key preceding it.
For example, imagine you have the message type:
message Test4 {
repeated int32 d = 4 [packed=true];
}
Now let's say you construct a Test4, providing the values 3, 270, and 86942 for the repeated field d. Then, the encoded form would be:
22 // key (field number 4, wire type 2)
06 // payload size (6 bytes)
03 // first element (varint 3)
8E 02 // second element (varint 270)
9E A7 05 // third element (varint 86942)
三、為什么不默認使用這種packed格式
這種設計從一開始就考慮到它的前后兼容屬性:也就是老的代碼解析添加字段之后的結構時是正確的,這個正確性主要表示能夠成功的讀出之前已經存在的字段。假設在外面存儲了長度,然后使用長度驅動進行逐個解析,那么當這些結構中間添加了某個字段之后,逐個解析就會出錯。解決的方法就是把整個結構掰開揉碎,每次只認TAG整個唯一標准。當遇到不識別的TAG是直接跳過,從而保證“未來兼容”。
考慮這么一個結構
message sub
{
int32 i = 1;
};
message main
{
repeated sub ss = 1;
};
如果對於main結構,ss包含4個元素{1,2,3,4}。此時生成結構為
06 04 01 02 03 04
之后為sub添加一個新的字段,
message sub
{
int32 i = 1;
float f = 2;
};
那么新生成的大概為
06 04 01 00 02 00 03 00 04 00
此時老的客戶端解析這個數據流就會有問題。
四、packed的兼容性
對於這種解消息定義
message mainmsg
{
int32 x = 1;
submsg msg = 2;
repeated submsg msgarr = 3;
repeated int64 repeatint = 4;
};
在對應的解析代碼中,可以看到,它是通過靜態的判斷TAG的數值來決定此時的數據流是否是packed的,因為不同類型它們編碼生成的TAG值並不相同。
#if GOOGLE_PROTOBUF_ENABLE_EXPERIMENTAL_PARSER
const char* mainmsg::_InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) {
while (!ctx->Done(&ptr)) {
……
// repeated int64 repeatint = 4;
case 4: {
if (static_cast<::PROTOBUF_NAMESPACE_ID::uint8>(tag) == 34) {
ptr = ::PROTOBUF_NAMESPACE_ID::internal::PackedInt64Parser(mutable_repeatint(), ptr, ctx);
GOOGLE_PROTOBUF_PARSER_ASSERT(ptr);
break;
} else if (static_cast<::PROTOBUF_NAMESPACE_ID::uint8>(tag) != 32) goto handle_unusual;
do {
add_repeatint(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint(&ptr));
GOOGLE_PROTOBUF_PARSER_ASSERT(ptr);
if (ctx->Done(&ptr)) return ptr;
} while ((::PROTOBUF_NAMESPACE_ID::internal::UnalignedLoad<::PROTOBUF_NAMESPACE_ID::uint64>(ptr) & 255) == 32 && (ptr += 1));
break;
}
……
}
五、使用packed弊端
明顯的,使用packed正如名字所暗示的:它的壓縮性更好。但是它的限制在於它只能添加在基礎的varint、int32、int64等類型,所以擴展性是一個問題。
例如
message sub
{
int32 i = 1;
};
message main
{
repeated sub ss = 1;
};
這種結構,之后可以方便的以sub為單位擴展,在里面添加字段。
單如果定義為
message main
{
repeated int ss = 1;
};
那么之后添加字段的時候就很麻煩。
六、舉例說明
1、在使用packed(默認為true)的情況下
message mainmsg
{
int32 x = 1;
submsg msg = 2;
repeated submsg msgarr = 3;
repeated int64 repeatint = 4;
};
repeatint中添加三個0x66,其序列化之后內容為
22 03 66 66 66
2、禁用packed之后
message mainmsg
{
int32 x = 1;
submsg msg = 2;
repeated submsg msgarr = 3;
repeated int64 repeatint = 4 [packed=false];
};
同樣內容對應的輸出為
20 66 20 66 20 66
3、更好的兼容
message sub
{
int64 x = 1;
};
message mainmsg
{
int32 x = 1;
submsg msg = 2;
repeated submsg msgarr = 3;
repeated sub repeatint = 4;
};
對應編碼為
22 02 08 66 22 02 08 66 22 02 08 66
其中的22為TAG編碼(TAG為4,wire類型為length-delimited數值為2);接下來02為接下來字節數,后面跟了兩個字節;接下來08為(1<<3 + wiretype(0)),之后才是真正的數據內容0x66。
可以看到,這個擴展性最高的結構比packed相比存儲效率還是低很多的。