varint压缩算法详解

varint是一种对正整数进行可变长字节编码的方法,大多数情况下可起到数据压缩的作用。通常,一个int型整数占4个字节,若该整数的数值小于256,显然一个字节的空间就能存储,浪费了3个字节的空间,而varint就起到了压缩数据的作用。整数数值越小,需要存储的字节数就越少。

varint原理

uint32_t = 1;
// 原码:0000 0000 0000 0000 0000 0000 0000 0001
// 补码:0000 0000 0000 0000 0000 0000 0000 0001
// varints编码:0|000 0001(0x01)
//

// 整数1的variant解码
// 读取到0x01, msb位为0, 说明读取结束,该数就只占一个字节.
// 读到7bits值为000 0001的补码,显然其原码是000 0001,也就是数值1
// 如此,便将variant编码的整数1解码出来了。而0x01只用到了一个字节哦!
uint32_t = 665;
// 原码:0000 0000 0000 0000 0000 0010 1001 1001
// 补码:0000 0000 0000 0000 0000 0010 1001 1001
// 按照7bit一组,0000 ... 0000101 0011001
// 从低位往高位依次取7bit,重新编码,注意是反转排序,全0的bit舍弃
// 0011001 0000101
// 再加上msb,每7bit组 组成一个字节
// varints编码即为:
// 1|0011001 0|0000101
// 0x99       0x05
// 如此,即将整数665编码成0x99 0x05,将原来的4个字节压缩成2个字节

// 再来看看解码过程
// 编码为 0x99 0x05, 第一个字节的msb位为1,表示继续读取下一个字节;第二个字节的msb为0,读取结束。
// 读出两个字节的内容后,去掉msb,还原成2组 7bits组,即 0011001 0000101
// 再将两组7bits反转,即 0000101 0011001 (补码)
// 由于是整数,所以得到原码 0010 1001 1001,即665

我们知道variant是可变长的编码方式,但是,它总得知道要读取多少个字节结束啊,总不能一直读下去吧。通常我们需要指定length信息,而variant编码天然包含了length,也就是通过msb位来识别,如果某个数的variant编码的字节的msb位为1,就意味着还要继续读取,没有结束,直到后面的某个字节的msb为0。正是msb的存在可以让数值小的数占用更少的空间,提高了空间利用率。由于每个字节都拿出一个bit做为msb,那么,4个字节最大额能表示的数为 2 28 2^{28} 228,而不是 2 32 2^{32} 232,在实际情况下,大于 2 28 2^{28} 228的数很少出现,所以在实际工程中并不会影响高效性。

int32_t val = -1 // 4字节
// 原码:1000 0000 0000 0000 0000 0000 0000 0001
// 补码:1111 1111 1111 1111 1111 1111 1111 1111
// 按照variant的编码规则,7bit一组,低位在前
// 补码的4个字节,共32bit,按7bit分组,可分为4组,这4组的msb均为1;余下的4bit在第5组,msb为0,其余位补0
// 1111111 1111111 1111111 1111111 1111
// 1|1111111 1|1111111 1|1111111 1|1111111 0|0001111
//   0xFF      0xFF      0xFF     0xFF       0x0F
// 所以,-1的varint编码为 0xff 0xff 0xff 0xff 0xf 共5字节

从上例来看,原本占4字节的-1,经variant编码后,占了5个字节。并没有压缩呀!这就是variant编码的缺陷:variant编码对负数编码效率低。通常可使用zigzag编码将负数转为正数,然后再用variant编码

编码规则

  1. 将整数的二进制补码按7bit分组,从低位往高位依次排列;
  2. 除最后一组的msb位置0,其他组的msb置1;

已知编码规则,解码就是反向的过程,一是要去掉msb重新组合7bit组,二是7bit组要反序存储。

至此,varint编码的原理已经阐述清楚。接下来就是编码实现。

varint编码C实现

#define VARINT_FIX (0x80)
uint8_t* varint_encode(uint32_t val, uint8_t *ptr)
{
    if (val < VARINT_FIX)
    {
        ptr[0] = (uint8_t)val;
        return ptr+1;
    }
    ptr[0] = (uint8_t)(val|VARINT_FIX);
    val >>= 7;

    if (val < VARINT_FIX)
    {
        ptr[1] = (uint8_t)val;
        return ptr+2;
    }
    ptr++;
    do
    {
        *ptr = (uint8_t)(val|VARINT_FIX);
        val >>= 7;
        ++ptr;

    }while(val >= VARINT_FIX);
    *ptr++ = (uint8_t)val;

    return ptr;
}

获取varint码的长度

// 获取varint编码的长度
uint8_t varint_code_len(uint8_t *data)
{
    uint8_t i = 0;
    while(data[i++]>>7);
    return i;
}

varint码 解码C实现

// varint解码函数
void varint_decode(uint8_t *data, uint32_t* val, uint8_t len)
{
    int i = 0;
    int offset = 0;
    uint32_t result = 0;
    result |= ((uint32_t)data[len-1]) << (7*(len-1));
    for (i = 0, offset = 0; i < len-1; i++,offset +=7)
        result |= (uint32_t)(data[i] & 0x7f) << offset;
    *val = result;
}

实际运行结果如下:

$ ./a.out 6650
input integer:6650,0x19fa, 4 Bytes
varint code len:2 Bytes
varint code is:0xfa33
decode val:6650

# 理论推导一下
# 6650(0x19fa)
# 原码:0001 1001 1111 1010
# 补码:0001 1001 1111 1010
# varint码:1|1111010 0|0110011
#             0xfa      0x33
$ ./a.out 1234567
input integer:1234567,0x12d687, 4 Bytes
varint code len:3 Bytes
varint code is:0x87ad4b
decode val:1234567

# 理论推导一下
# 1234567(0x12d687)
# 原码:0001 0010 1101 0110 1000 0111
# 补码:0001 0010 1101 0110 1000 0111
# varint码:1|0000111 1|0101101 0|1001011
#              0x87      0xad     0x4b 

$ ./a.out -1
input integer:4294967295,0xffffffff, 4 Bytes
varint code len:5 Bytes
varint code is:0xfffffffff
decode val:4294967295

# 从运行结果可以看出,-1通过varint编码后,占5个字节的空间,和前面的理论分析一致。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sif_666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值