zoukankan      html  css  js  c++  java
  • protobuf中packed字段的使用

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

  • 相关阅读:
    hdoj 5311 Hidden String(KMP)
    hdoj 1532 Drainage Ditches(最大网络流)
    ubuntu中php+mysql环境搭建
    android:clipToPadding 和 android:clipChildren
    Js实现动画框架
    JavaScript初步认识
    JavaScript鼠标进入与退出监听动画
    Android框架集合
    Android 6.0(Android M)动态授权
    Android应用申请ROOT权限
  • 原文地址:https://www.cnblogs.com/tsecer/p/14358934.html
Copyright © 2011-2022 走看看