4. 变长编码(VByte):原理、算法与倒排列表应用

VByte(Variable Byte)是倒排索引压缩中最经典的变长编码之一。它用每个字节的最高位(continuation bit)标记当前字节是否为某个整数的最后一个字节,从而用 1~5 个字节表示任意 32 位无符号整数。VByte 在解压速度上远优于位对齐编码(如 Golomb、Rice),且压缩比适中,是搜索引擎中“以速度换空间”的常用方案。

4.1 VByte 编码原理

VByte 将每个整数拆分为若干 7 位有效数据块,每个数据块存入一个字节的低 7 位,字节的最高位作为“延续位”:

  • 延续位 = 1:当前字节是整数的最后一个字节。
  • 延续位 = 0:后面还有后续字节。

编码时从整数的低位开始,每次取 7 位,直到所有位处理完毕。最后一个字节的延续位置 1,其余字节的延续位置 0。解码时依次读取字节,检查延续位,若为 0 则继续读取,若为 1 则拼接出完整整数。

关键特性: VByte 是字节对齐的,每个编码单元都是完整字节,因此解码时无需位操作,仅需移位和按位或运算,在现代 CPU 上可达到 1~2 GB/s 的解压吞吐。
VByte 编码结构:整数 123456 → 3 字节 原始整数 123456 (二进制): 0000 0000 0000 0001 1110 0010 0100 0000 按 7 位分组(从低位开始): 组3: 0000011 | 组2: 1100010 | 组1: 1000000 字节 1 0 1000000 字节 2 0 1100010 字节 3 1 0000011 延续位=0 延续位=0 延续位=1

4.2 编码与解码算法步骤

4.2.1 编码算法

  1. 输入:32 位无符号整数 n。
  2. 循环:当 n >= 128 时,取 n 的低 7 位,与 0x80 按位或(延续位=0),存入输出缓冲区;n >>= 7。
  3. 最后字节:将剩余的 n 与 0x80 按位或(延续位=1),存入输出缓冲区。
  4. 输出:字节序列,长度 1~5 字节。
// C 风格 VByte 编码
void vbyte_encode(uint32_t n, uint8_t* &out) {
    while (n >= 128) {
        *out++ = (n & 0x7F) | 0x80;  // 延续位=0
        n >>= 7;
    }
    *out++ = n | 0x80;  // 延续位=1
}

4.2.2 解码算法

  1. 输入:VByte 编码的字节流。
  2. 循环:读取一个字节 b,取低 7 位拼接到结果 val 的高位;若 b & 0x80 == 0,继续读取下一个字节;若 b & 0x80 != 0,解码结束。
  3. 输出:还原的 32 位整数 val。
// C 风格 VByte 解码
uint32_t vbyte_decode(const uint8_t* &in) {
    uint32_t val = 0;
    int shift = 0;
    uint8_t b;
    do {
        b = *in++;
        val |= (uint32_t)(b & 0x7F) << shift;
        shift += 7;
    } while ((b & 0x80) == 0);
    return val;
}
性能要点: 解码循环中每次迭代处理一个字节,无分支预测惩罚(延续位检查是固定模式),且移位量每次增加 7,编译器可优化为常量加法。实测单核解码速度可达 1.5 GB/s 以上。

4.3 VByte 在倒排列表中的应用

倒排列表存储的是文档 ID 序列和词频(TF)序列。直接存储原始 ID 会导致大量冗余,因此搜索引擎通常先对文档 ID 进行 差值(d-gap)转换,再对差值序列进行 VByte 编码。

步骤 操作 示例(文档 ID 序列)
1. 原始 ID 直接存储 [42, 100, 101, 205, 1000]
2. d-gap 转换 相邻 ID 求差 [42, 58, 1, 104, 795]
3. VByte 编码 对每个差值编码 42→1字节, 58→1字节, 1→1字节, 104→2字节, 795→2字节
4. 存储 字节流写入倒排文件 总字节数:1+1+1+2+2 = 7 字节

词频(TF)通常与文档 ID 交替存储或使用独立流。由于词频值一般较小(多数为 1~10),VByte 编码后大多只需 1 个字节,压缩效率极高。

VByte 在倒排列表中的编码流程 原始倒排列表 [42, 100, 101, 205, 1000] d-gap 转换 [42, 58, 1, 104, 795] VByte 编码 7 字节流 压缩倒排 文件 VByte 压缩效果示例 • 原始 5 个文档 ID 需 20 字节(4 字节/ID) • d-gap 后 VByte 编码仅 7 字节,压缩比 ≈ 2.86:1 • 解码时逐字节读取,无需位操作,解压速度极快

4.4 VByte 的优缺点与适用场景

维度 评价
压缩比 中等。小整数(1~127)仅 1 字节,大整数(2^21~2^28)需 4 字节。平均压缩比约 2~3:1。
解码速度 极快。字节对齐、无位操作、无分支预测惩罚,单核可达 1.5 GB/s+。
实现复杂度 极低。编码/解码各约 5~10 行代码,无复杂数学运算。
适用场景 对延迟敏感、需要快速随机访问的搜索服务;倒排列表较短或差值较小的场景。
局限性 大整数(如 d-gap 超过 2^21)会膨胀到 4 字节,压缩比不如位对齐编码(如 Simple-9、PForDelta)。
尾延迟影响提示: VByte 的解码速度高度稳定,几乎不受数据分布影响。在压测中,VByte 的尾延迟(p99/p999)通常比位对齐编码低 30%~50%,但压缩后倒排文件体积可能增大 10%~20%,导致 I/O 次数增加。实际选型需在“压缩比”与“解码速度”之间权衡。