前言
最近在学习leveldb,遇到varint这种数据结构,本来想直接看网上的解析,然而网上的解析都千篇一律,都是贴代码让自己去看。好不容易弄懂之后决定把思考过程贴出来,方便大家学习。
leveldb中为了减少内存占用,使用了varint这一数据结构,把数字放到字符数组中来表示,思想就是把小数字用尽量少的字节来表示,每个字节只使用其中的7位,最高位用来表示是否还有剩余的数字,0代表没有,1代表有。
encode
代码
下面是encode
的代码
#include <stdio.h>
#include <inttypes.h>
#include <string>
char *encode_varint32(char *dst, uint32_t value) {
uint8_t *ptr = reinterpret_cast<uint8_t*>(dst);
// B = 0b 1000 0000
const int B = 128;
if(value < (1 << 7)) {
*(ptr++) = value;
} else if (value < (1 << 14)) {
*(ptr++) = value | B;
*(ptr++) = value >> 7;
} else if (value < (1 << 21)) {
*(ptr++) = value | B;
*(ptr++) = value >> 7;
*(ptr++) = value >> 14;
} else if (value < (1 << 28)) {
*(ptr++) = value | B;
*(ptr++) = value >> 7;
*(ptr++) = value >> 14;
*(ptr++) = value >> 21;
} else {
*(ptr++) = value | B;
*(ptr++) = value >> 7;
*(ptr++) = value >> 14;
*(ptr++) = value >> 21;
*(ptr++) = value >> 28;
}
return reinterpret_cast<char*>(ptr);
}
讲解
首先把字符数组转为uint8_t
类型的数组,这样我们接下来才能表示数字。可以看到,在判断语句中对数字所需要的位数进行了判断,如果可以用7个位来表示,那就直接赋值,如果可以用14个位来表示(剩下两位要用来表示数字是否结束),那就把数字的前7位放到dst[0]
中,在把剩余的7位右移,放到dst[1]
中去。如果可以用21个位来表示,那么前七个位放到dst[0]
,中间七位放到dst[1]
,最后七位放到dst[2]
。
举例说来,假设我们的dst
是一个长度位5的字符数组,value
等于129。那么dst
在内存空间中是这样表示的dst[4] dst[3] dst[2] dst[1] dst[0]
,每个占据8位,value的二进制表示为0000 0000 1000 0001
。ptr
首先指向dst[0]
,现在我们通过*(ptr++) = 128 | value
把最后七位0000 0001
放到dst[0]
中去,此处发生了截断,因为*ptr
是uint8_t
类型,而value
是uint32_t
,所以只会把value
的最后七位放到dst[0]
中去,放完之后dst[0]
等于1000 0001
,注意此处最高位的1并不是数字,而是代表数字没有结束,要继续操作。接下来我们把中间七位00 0000 1
放到dst[1]
中去,首先右移七位得到0000 0001
,然后直接赋值即可。
decode
代码
void decode_varint32(const char *src, /*const char *limit,*/ uint32_t *value) {
const char *p = src;
uint32_t v = *reinterpret_cast<const uint8_t*>(p);
const int B = 128;
*value = 0;
uint8_t shift;
for(shift = 0; shift <= 28 && (v&B)/* p < limit*/ ; shift += 7) {
//0x7f = 0b 0111 1111
*value |= ((v & 0x7f) << shift);
p++;
v = *reinterpret_cast<const uint8_t*>(p);
}
if(shift <= 28) {
*value |= ((v & 0x7f) << shift);
}
}
讲解
decode
就是encode
的逆过程,每次从数组中拿出七位,移位到最终的位置后与结果按位或,直到字节的最高位为0代表结束即可。
举例说来,刚才我们把129放到了dst
中,现在dst
的内容是:dst[0]
存放1000 0001
,dst[1]
存放0000 0001
。ok,我们现在从dst[0]
开始取,因为要消去最高位的标识位,所以要和0111 1111
按位与即& 0x7f
,dst[0]
代表最后七位,移位0位即可,现在我们的*value
等于1。接下来对dst[1]
进行循环条件判断,一位内dst[1]=0000 0001
不符合条件,进入最后的if
语句,在这里我们接着取dst[1]
的内容,因为dst[1]
中存放中间七位,我们取出数据后消去标志位,然后左移七位得到1000 0000
,然后与刚才的结果按位或得到最终结果129。
这里我对leveldb中的代码(源码见GetVarint32PtrFallback)做了些改动,看起来更简洁一些,另外我觉得leveldb中的const char *dst
是不必要的,因为已经有了最高位来判断数字是否结束,当然,我们写代码的时候最好还是加上,更严格的控制条件并没什么不好,相反可能还会有好处,这里只是提供一个另外的思路。
测试
int main() {
char buf[5] = {0};
char *ptr = encode_varint32(buf, 279);
uint32_t b = 0;
decode_varint32(buf, /*ptr,*/ &b);
printf("%" PRIu32"", b);
encode_varint32(buf, 876);
decode_varint32(buf, &b);
printf("%" PRIu32"\n", b);
encode_varint32(buf, 65532);
decode_varint32(buf, &b);
printf("%" PRIu32"\n", b);
encode_varint32(buf, 23456);
decode_varint32(buf, &b);
printf("%" PRIu32"\n", b);
}