【Protobuf(三)】Varints 与 Zigzag 编码

在计算机中,整数值通常有两种类型:32位以及64位,在Java中分别对应的是int和long,在C++中分别对应的是int32/uint32和int64/uint64。整型值按照二进制补码的形式存放。而且是定长的。也就是不论数值大小,类型一旦固定,占用的bit位数就是固定的。

那么protobuf协议如何序列化一个整数值?

protobuf使用Varints编码格式来编码整数,这个协议最大的特点是变长编码。怎么理解?也就是一个整数值编码后的字节数是不确定的,数值越小,占用的字节数也越少,比如127以内的数值,只需要一个字节,数值越大占用的字节数也越大。Varints编码需要征用每一个字节的最高位作为标记为,简称MSB,所以一个字节实际上只有7个bit在实际编码数据。那这个MSB位的作用是啥?是用来表示是否还需要读取下一个字节:如果是0,表示不需要,否则表示需要。这个思路有点类似utf-8编码。另外,Varints是小端序,需要将字节翻转。

看几个例子:

7 = 00000111 -> 7,可以在7个bit内表示,共占用一个1字节;

320 = 256 + 64 = 101000000(补码表示) -> 0000010 1000000(7bit分组) -> 00000010 11000000(从右向左标记MSB) -> 11000000 00000010(字节翻转) -> -64 2

所以,我们可以看到7使用了1字节,320会用了2字节。在数值较小时,还是很省空间的。

如果是要表示负数呢?

我们知道负数在补码中首位会是连续的1,也就是任意一个负数,开始都会是连续的1。这样的格式在Varints编码下会是怎样的?

举个例子:

以Java里的int类型为例,-64 = 11111111 11111111 11111111 11000000

虽然绝对值很小,但是补码整整占用了4个字节,在转为Varints编码后需要5个字节才能表示,而且不论负数的数值多大,都稳定占用5个字节。(这里说明一下,实际上在protobuff中,负数会稳定占用10个字节,也就是会按照long来编码,这里为了简化,暂且以5个字节来说明。至于为啥是10个字节,是因为如果将int32改为了int64,仍然可以兼容)。

所以说,在表示负数时,Varints会退化为定长编码,不够高效。为了解决这个问题,引入了Zigzag编码,我们看一下:核心思路是将负数转化为其绝对值再进行Varints编码。怎么转化?0, -1, 1, -2对应为0, 1, 2, 3等等

对应关系如上,类似一个拉链,所以才叫Zigzag。

那如何知道一个字节流使用了Zigzag编码?

protobuff里引入了sint32/sint64类型,只有声明为这两个类型才会对负数先使用Zigzag编码,否则直接使用Varints编码。

接下来看一下几种整数类型的区别(https://developers.google.com/protocol-buffers/docs/proto?csw=1#scalar):

有符号的:int32/int64、sint32/sint64

无符号:uint32/uint64

在Java里,由于没有无符号整数,所以uint和int是一样的。在C++中会有区别。那这几个类别怎么选择?

https://stackoverflow.com/questions/765916/is-there-ever-a-good-time-to-use-int32-instead-of-sint32-in-google-protocol-buff

如果确定没有负数,可以选择uint。在Java里,uint和int一样,所以选择两个都可以。

如果有负数,且出现频率较高,可以选择sint。否则选择int,毕竟sint还需要多一次编码。

另外,补码数值在转为字节流时,其实不关心是uint还是int,以及是int32还是int64,之所以需要区分这些类型,是因为需要在反序列时,知道转为对应语言下的哪一个类型。比如一个数值i=6,在int32和int64下序列化时一样的,同一个值需要反序列为int还是long,是根据pb里定义的类型来定的。

关于同一个值下的int32和int64序列化后的字节流一样的例子:

message TestMsg1 {
    int32 a = 1;
    int64 b = 2;
}

@Test
public void test4() {
    MyProto.TestMsg1 msg = MyProto.TestMsg1.newBuilder()
            .setA(4)
            .setB(4L)
            .build();
    printHex(msg.toByteArray());
}

值为:8, 4, 16, 4。所以int的4和long的4,使用Varints序列化后的字节是一样的。int32和int64的却别在于反序列化时表示语言里的数据类型。

 

小结一下:

定长编码:一视同仁,所有数值都是相同空间,通过类型即可确定占用空间大小;变长编码:根据数值大小调整占用的空间,但是需要一个标识空间大小的信息,Varints是通过征用MSB位的方式;

Zigzag编码:Varints在表示负数时会退化为定长编码,引入了Zingzag及sintxxx,对绝对值Varins编码。如果负数较多,最好用sintxxx;

在Java里,intxxx和uintxxx一样,没有无符号整数。如果负数不多,用这俩就行,如果较多,最好用sintxxx;

 

参考链接:

https://juejin.im/post/6844903997292150791

https://juejin.im/post/6844904025578553351

 

https://www.jianshu.com/p/73c9ed3a4877

https://ngtzeyang94.medium.com/go-with-examples-protobuf-encoding-mechanics-54ceff48ebaa

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值