一、为什么使用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相比存储效率还是低很多的。