1. 转换格式
.proto Type | Notes | C++ Type | Java Type | Python Type[2] | Go Type |
---|---|---|---|---|---|
double | double | double | float | *float64 | |
float | float | float | float | *float32 | |
int32 | 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint32 |
int32 | int | int | *int32 |
int64 | 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint64 |
int64 | long | int/long[3] | *int64 |
uint32 | 使用可变长度编码 | uint32 | int[1] | int/long[3] | *uint32 |
uint64 | 使用可变长度编码 | uint64 | long[1] | int/long[3] | *uint64 |
sint32 | 使用可变长度编码。有符号的 int 值。这些比常规 int32 对负数能更有效地编码 | int32 | int | int | *int32 |
sint64 | 使用可变长度编码。有符号的 int 值。这些比常规 int64 对负数能更有效地编码 | int64 | long | int/long[3] | *int64 |
fixed32 | 总是四个字节。如果值通常大于 228,则比 uint32 更有效。 | uint32 | int[1] | int/long[3] | *uint32 |
fixed64 | 总是八个字节。如果值通常大于 256,则比 uint64 更有效。 | uint64 | long[1] | int/long[3] | *uint64 |
sfixed32 | 总是四个字节 | int32 | int | int | *int32 |
sfixed64 | 总是八个字节 | int64 | long | int/long[3] | *int64 |
bool | bool | boolean | bool | *bool | |
string | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本 | string | String | str/unicode[4] | *string |
bytes | 可以包含任意字节序列 | string | ByteString | str | []byte |
2. 原理分析
Protobuf采用了T-L-V的存储格式存储数据,其中的T代表tag,即key;L则是length,代表当前存储的类型的数据长度,当是数值类型的时候L被忽略;V代表value,即存入的值。
protobuf会将每一个key根据不同的类型对应的序列化算法进行序列化,然后按照keyvaluekeyvalue的格式存储,其中key的type类型与对应的压缩算法关系如下:
wire_type | 编码方式 | type | 存储方式 |
---|---|---|---|
0 | Varint(负数使用Zigzag辅助) | int32、int64、uint32、uint64、sint32、sint64、bool、enum | T-V |
1 | 64-bit | fixed、sfixed64、double | T-V |
2 | Length-delimi | string、bytes、embedded、messages、packed repeated fields | T-L-V |
3(弃用) | Start group | Groups(deprecated) | 弃用 |
4(弃用) | End group | Groups(deprecated) | 弃用 |
5 | 32-bit | fixed32、sfixed32、float | T-V |
需要注意的是protobuf的key计算按照 (field_number << 3) | wire_type 方式计算,而这里的field_number是指定义的时候该字段的域号,如:required string name=1;这里的name字段的域号为1,在protobuf中规定:
- 如果域号在[1,15]范围内,会使用一个字节表示Key;
- 如果域号大于等于16,会使用两个字节表示Key;
key编码完成后,该字节的第一个比特位表示后一个字节是否与当前字节有关系,即:
- 如果第一个比特位为1,表示有关,即连续两个字节都是Key的编码;
- 如果第一个比特位为0,表示Key的编码只有当前一个字节,后面的字节是Length或者Value;
注意:protobuf中的域号定义要小于2048 ,原因为,最大的域号即2个字节16个比特位表示key,去掉位移的三位,还剩下13位,再去掉两个字节开头的第一个用来表示是否存在关系的比特位,即16-3-2=11,最后只有11位参与计算,二进制计算后2^11== 2048 ,所以域号不得超过2048
2.1 varint编码
而我们都知道计算机中,高位为1代表负数,计算机中对负数的计算为先将结果取反后,再去补码操作,
而负数的补码则是在反码的基础上+1,那么我们现在将结果反过来,先去-1,得到反码,则为10101011,
再去取反,得到原码,则为01010100,
现在我们将这个值转换为十进制,则可以知道结果为84,由于高位为1,则代表是负数,最终结果为-84,
而00000010由于高位是0,代表本身为正数,正数的原码反码补码都是自身,所以直接转换为十进制结果为2,
现在我们把这两个结果和上述打印的结果比较一下,是不是发现是一样的?
当然,我们也从这个过程中发现了一些问题,比如小于128的值,我们甚至只需要1个字节就能存储完毕,
但是如果我们需要存储的值很大,超过了268435455以后的数值,甚至需要五个字节来存储(超过28个有效比特位),
但是绝大多数情况下,我们都不会使用这么大的数值,
所以一般情况下,我们都能比之前使用更小的字节存储,达到压缩的目的。
2.2 字符串压缩
- 负数存储- write_type为0
在计算机中,负数会被表示为很大的整数,因为计算机定义负数符号位为数字的最高位,
所以如果采用 varint 编码表示一个负数,那么一定需要 5 个比特位。
在 protobuf 中通过sint32/sint64 类型来表示负数,负数的处理形式是先采用 zigzag 编码
( 把符号数转化为无符号数 ),再采用 varint 编码。
sint32:(n << 1) ^ (n >> 31)
sint64:(n << 1) ^ (n >> 63)
比如存储一个(-300)的值
-300
原码:0001 0010 1100
取反:1110 1101 0011
加 1 :1110 1101 0100
n<<1: 整体左移一位,右边补 0 -> 1101 1010 1000
n>>31: 整体右移 31 位,左边补 1 -> 1111 1111 1111
n<<1 ^ n >>31
1101 1010 1000 ^ 1111 1111 1111 = 0010 0101 0111
十进制: 0010 0101 0111 = 599
然后再使用varint 算法得到两个字节
1101 0111(-41),0000 0100(4)
3. 总结
基于Protobuf序列化原理分析,为了有效降低序列化后数据量的大小,可以采用以下措施:
- 字段标识号(Field_Number)尽量只使用1-15,且不要跳动使用 Tag是需要占字节空间的。如果Field_Number>16时,Field_Number的编码就会占用2个字节,那么Tag在编码时就会占用更多的字节;如果将字段标识号定义为连续递增的数值,将获得更好的编码和解码性能
- 若需要使用的字段值出现负数,请使用sint32/sint64,不要使用int32/int64。 采用sint32/sint64数据类型表示负数时,会先采用Zigzag编码再采用Varint编码,从而更加有效压缩数据
- 对于repeated字段,尽量增加packed=true修饰 增加packed=true修饰,repeated字段会采用连续数据存储方式,即T - L - V - V -V方式
4. 参考文档