资讯动态

Folly GroupVarint 详解:面向倒排索引的分组变长整数编码方案

发布时间:2026/9/11 18:02:02 来源:尧图企业网站定制
Folly GroupVarint 详解面向倒排索引的分组变长整数编码方案【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/follyfolly/GroupVarint.h是 FacebookMeta开源 C 库 Folly 中提供的一种面向 32 位与 64 位整数的分组变长编码Group Varint Encoding实现其编码思想源自 Jeff Dean 在 WSDM 2009 大会上的主题演讲以及《Information Retrieval: Implementing and Evaluating Search Engines》一书对倒排索引压缩技术的论述。本篇文章将结合 folly/GroupVarint.h、folly/GroupVarint.cpp、folly/detail/GroupVarintDetail.h 与 folly/test/GroupVarintTest.cpp 的源码与测试完整讲解 GroupVarint 的编码格式、三大核心类GroupVarintT、GroupVarintEncoderT, Output、GroupVarintDecoderT的用法、SSSE3 指令加速原理与平台适配细节并对比传统 Varint如 folly/Varint.h 所实现的 base-128 变长编码帮助读者在实际项目中正确选型并高效使用这一编码方案。一、为什么需要 GroupVarint从传统 Varint 说起在搜索引擎、数据库与日志系统中海量的整数 ID如文档编号、词项编号、时间戳需要被序列化存储或传输。若直接以定长整数4 字节/8 字节存储空间浪费明显。变长编码Variable-Length Encoding应运而生。Folly 在 folly/Varint.h 中提供了经典的 base-128 Varint 实现每个字节用 7 位存放数据、最高位作为“是否还有后续字节”的标记32 位值最长占用 5 字节64 位值最长占用 10 字节。该头文件在注释中明确指出If you want to encode multiple values, GroupVarint (in GroupVarint.h) is faster and likely smaller.即当需要批量编码多个值时GroupVarint更快且通常更紧凑。这正是 GroupVarint 的定位——以“分组”为代价换取更高的吞吐率与更小的元数据开销。它在倒排索引Posting List压缩、ID 数组序列化等场景中尤为常见。二、GroupVarint 编码格式一个组一个头部字节与逐值携带长度信息的传统 Varint 不同GroupVarint 将一组整数一次性编码组内每个值采用 1 至 N 字节的“定长截断”表示截断掉高位多余的零而各组值的长度信息被集中打包进**组头header**中从而把长度元数据从每个值平摊到整个组上。2.1 32 位值每组 4 个整数5~17 字节对uint32_t而言一组固定编码 4 个整数编码结果长度为 517 字节第 1 字节为组头其中每 2 位表示一个整数的长度编码值00表示 1 字节、01表示 2 字节、10表示 3 字节、11表示 4 字节后 4 段为 4 个整数实际的有效字节小端序。组头的位布局在 folly/GroupVarint.cpp 中有明确的注释说明bit 0..1第一个整数的长度减 1bit 2..3第二个整数的长度减 1bit 4..5第三个整数的长度减 1bit 6..7第四个整数的长度减 1。GroupVarint32::size()的计算公式为1组头 4四个整数各至少 1 字节 key(a) key(b) key(c) key(d)其中key(x)返回该值超出 1 字节的额外字节数。全零组仅需 5 字节而 4 个接近UINT32_MAX的值则需要 17 字节1 字节组头 16 字节数据。2.2 64 位值每组 5 个整数7~42 字节对uint64_t而言一组固定编码 5 个整数编码结果长度为 742 字节前 2 字节为组头其中每 3 位表示一个整数的长度编码值000表示 1 字节、……、111表示 8 字节15 位刚好容纳 5 × 3 位后 5 段为 5 个整数实际的有效字节。对应地组头中第 j 个整数的长度编码通过bJkey(x)以(x (3*j)) 7的方式取出。全零组仅需 7 字节而 5 个接近UINT64_MAX的值则需要 42 字节2 字节组头 40 字节数据。2.3 编码示例来自测试用例folly/test/GroupVarintTest.cpp 的testGroupVarint32与testGroupVarint64直接给出了逐字节的期望结果是理解编码格式的最佳教材。以 32 位组为例// 4 个值256, (216)3, (424)(58)6, 7 // 编码结果16 进制0x39, 0, 1, 3, 0, 2, 6, 5, 0, 4, 7逐字节解读组头0x39的二进制为00 11 10 01即四段长度编码依次为01第 1 值 2 字节、10第 2 值 3 字节、11第 3 值 4 字节、00第 4 值 1 字节与 4 个值的实际字节数完全吻合随后的数据区依次为各值的小端字节序列。测试通过EXPECT_EQ(expectedBytes.size(), size)同时校验了size()的计算与逐字节的编码输出。三、核心类一GroupVarintT——单组编解码接口GroupVarintTT为uint32_t或uint64_t是面向“一次处理一个组”的基础编解码接口全部为静态方法。它在 folly/GroupVarint.h 中提供了两组模板特化GroupVarintuint32_t别名GroupVarint32GroupVarintuint64_t别名GroupVarint64。两个特化共同继承自 folly/detail/GroupVarintDetail.h 中的detail::GroupVarintBaseT后者通过GroupVarintTraitsT定义了分组参数uint32_t对应kGroupSize 4、kHeaderSize 1uint64_t对应kGroupSize 5、kHeaderSize 2。GroupVarintBase还提供了两个通用工具方法maxSize(n)计算编码 n 个值所需的最大缓冲区字节数整组按最大长度kHeaderSize sizeof(T)*kGroupSize估算尾部不足一组的按单值最大长度累计totalSize(p, n)给定已编码的缓冲区与 n返回精确的占用总字节数。3.1 常用静态方法一览方法功能说明size(a, b, c, d)/size(a, b, c, d, e)返回编码这一组值所需的精确字节数size(const T* p)由数组指针计算一组值的编码大小encode(char* p, ...)编码一组值到缓冲区返回缓冲区的下一个写入位置即末尾之后的指针decode(const char* p, ...)从缓冲区解码一组值返回下一个读取位置decode_simple(...)不依赖 SSE 的标量解码版本见下文第五节encodedSize(const char* p)仅凭缓冲区首字节组头即可获知整组编码长度partialSize(p, count)计算编码组内前 count≤ 组大小个值所需字节数partialCount(p, size)给定缓冲区大小返回可安全解码出的有效值个数3.2 基本用法示例#include folly/GroupVarint.h // 编码一组 4 个 uint32_t uint32_t vals[4] {1, 2, 3, 4}; char buf[GroupVarint32::kMaxSize]; // 32 位组最大 17 字节 char* end GroupVarint32::encode(buf, vals); size_t bytes end - buf; // 实际编码字节数 CHECK_EQ(bytes, GroupVarint32::size(vals)); CHECK_EQ(bytes, GroupVarint32::encodedSize(buf)); // 解码回 4 个值 uint32_t out[4]; const char* next GroupVarint32::decode(buf, out);注意源码对缓冲区大小的要求encode要求缓冲区至少有size() 4字节可用32 位或size() 8字节可用64 位decodeSSE 路径要求输入指针之后至少可读 17 字节——因为_mm_loadu_si128会一次性读取 128 位16 字节加上组头共 17 字节。即使数据不足 17 字节多读的字节也会被忽略但内存必须可访问。这也是 folly/test/GroupVarintTest.cpp 中测试缓冲区总是先resize到不小于 17 字节的原因。四、核心类二/三流式编码器与解码器当数据不是整组到达而是逐值产生、逐值消费时使用GroupVarintEncoderT, Output与GroupVarintDecoderT可以自动完成组缓冲与不完整尾组的处理。4.1GroupVarintEncoderT, Output逐值输入按组冲刷输出模板参数T为uint32_t或uint64_tOutput是接受StringPiece参数的函数对象functor编码器内部攒满一组后会把该组的编码结果通过out(StringPiece)回调写出。关键成员add(val)加入一个值内部缓冲满kGroupSize个后自动编码并调用Output冲刷finish()显式结束编码将缓冲区中不足一组的剩余值按“部分组”写出finish()之后编码器可立即复用继续编码新数据析构函数会自动调用finish()因此即使忘记显式收尾不完整的尾组也会被正确落盘clear()丢弃当前缓冲状态已冲刷到输出的数据不受影响output()返回底层 Output 对象引用。源码中finish()的实现细节值得注意对于不足一组的剩余值它先把缓冲区内剩余槽位填 0 后执行一次完整encode再仅写出partialSize(buf_, count_)个字节。注释说明这样处理可以保证未初始化字节在编码时被记录为“1 字节”而非更多从而让partialSize的字节数计算与解码端partialCount严格对应——这正是测试中“裁剪缓冲区后仍可解码出前 count 个值”行为得以成立的关键。示例与测试用例同构的StringAppender模式#include folly/GroupVarint.h #include string struct StringAppender { std::string s; explicit StringAppender(std::string str) : s(str) {} void operator()(folly::StringPiece sp) { s.append(sp.data(), sp.size()); } }; std::string out; { folly::GroupVarintEncoderuint32_t, StringAppender enc(StringAppender(out)); enc.add(1); enc.add(2); enc.add(3); enc.add(4); // 攒满一组立即写出 5 字节 enc.add(5); // 只剩 1 个值析构时按部分组写出 } // out 中包含两组编码完整组(1,2,3,4) 部分组(5)4.2GroupVarintDecoderT逐值提取兼容不完整尾组GroupVarintDecoderT是流式解码器构造时接收StringPiece data与可选的maxCount限制最多解码出的值个数默认无限制。核心方法是bool next(T* val)返回false表示数据已耗尽或达到maxCount上限。为应对“最后一段数据凑不满一组”的情况解码器内部做了两层处理见 folly/GroupVarint.h 中next()的实现越界保护GroupVarint32::decodeSSE 路径需要组尾之后还能安全读取最多 16 字节。若剩余数据不足kMaxSize32 位为 17解码器会先将剩余字节memcpy进内部临时缓冲区tmp_大小为2 * kMaxSize再解码避免越界访问部分组判定当Base::decode返回的指针超过end_即解码出的组超出实际数据范围时改用partialCount(p_, rem)计算实际有效值个数若剩余值不足整组则通过partialSize精确定位本次消费的字节数。rest()方法用于在next()返回false后取回“未被消费的剩余数据”其指向原始输入数据的子区间而非内部临时缓冲区。测试用例GroupVarintDecoder中展示了maxCount的两种边界行为// 数据为两组共 9 字节第一组 4 个值 第二组 4 个值 1 字节开头 folly::StringPiece p(s.data(), 9); // 情况一maxCount3整组被截断 folly::GroupVarint32Decoder gv1(p, 3); uint32_t v; while (gv1.next(v)) { /* 读到 3 个值后停止 */ } // gv1.rest() \x04\x01\x02\x03\x045 字节 // 情况二maxCount5跨组读取 folly::GroupVarint32Decoder gv2(p, 5); while (gv2.next(v)) { /* 读到 5 个值后停止 */ } // gv2.rest() \x041 字节五、性能关键SSSE3 PSHUFB 指令加速 32 位解码GroupVarint 之所以在批量场景下显著快于逐值 Varint除了头部集中带来的带宽节省还因为其 32 位解码路径在支持 SSSE3FOLLY_SSE 4的平台上使用了PSHUFB_mm_shuffle_epi8指令完成单指令多数据SIMD重排。folly/GroupVarint.h 中SSE 版decode的核心逻辑是uint8_t key uint8_t(p[0]); // 读取组头 __m128i val _mm_loadu_si128((const __m128i*)(p 1)); // 一次加载后续 16 字节 __m128i mask _mm_load_si128( (const __m128i*)detail::groupVarintSSEMasks[key].data()); // 查表取 shuffle 掩码 __m128i r _mm_shuffle_epi8(val, mask); // 一次完成 4 个值的字节重排 _mm_storeu_si128((__m128i*)dest, r); return p detail::groupVarintLengths[key]; // 表查整组长度detail::groupVarintSSEMasks是一张 256 项的std::arraystd::arrayuint32_t, 4, 256查找表以组头字节为索引detail::groupVarintLengths则是 256 项的字节长度表。这两张表都在 folly/GroupVarint.cpp 中通过 constexpr 函数在编译期生成group_varint_table_sse_mask_make_item根据组头每 2 位长度编码把每个整数的有效字节映射到结果寄存器对应槽位其余字节置 00xff表示清零group_varint_table_length_make_item累加 4 个长度编码得到整组总字节数。例如组头值为 4二进制00 00 01 00时表示第 1 个整数占 1 字节、第 2 个占 2 字节、第 3、4 个各占 1 字节。源码注释给出了生成的掩码语义r[0]a[0]r[1..3]0r[4]a[1]r[5]a[2]r[6..7]0……即一次 PSHUFB 就把 4 个变长值展开为 4 个规整的 4 字节整数并补齐高位零。这一技术源自 CIKM 2011 的论文《SIMD-Based Decoding of Posting Lists》。正因为 SSE 路径对 32 位解码收益巨大源码注释特别强调32 位实现显著快于 64 位实现64 位路径仍为逐值标量处理未使用 PSHUFB。在不支持 SSE4 的平台上FOLLY_SSE 4decode自动退化为decode_simple标量版本——它逐个loadUnaligneduint32_t并借助掩码表kMask[]定义于 folly/GroupVarint.cpp0xff、0xffff、0xffffff、0xffffffff清除超出有效长度的字节。kMask同时服务于 64 位路径提供 18 字节共 8 级掩码。六、平台适配与依赖GroupVarint 并非在所有平台可用。源码顶部的条件编译定义了可用性开关#if FOLLY_X64 || defined(__i386__) || FOLLY_PPC64 || FOLLY_AARCH64 || FOLLY_RISCV64 #define FOLLY_HAVE_GROUP_VARINT 1 #else #define FOLLY_HAVE_GROUP_VARINT 0 #endif即 GroupVarint 仅面向 x86/x86-64、PowerPC 64、AArch64 与 RISC-V 64 架构启用且头文件要求编译器为 GCC 或 MSVC其他编译器会触发#error GroupVarint.h requires GCC or MSVC。测试代码 folly/test/GroupVarintTest.cpp 同样以#if FOLLY_HAVE_GROUP_VARINT包裹在不支持的平台上整个测试会被编译掉。此外实现依赖小端序与非对齐访问32 位路径通过storeUnaligneduint32_t/loadUnaligneduint32_t直接读写非对齐内存64 位路径通过storeUnaligneduint64_t/loadUnaligneduint64_t以及loadUnaligneduint16_t读取 2 字节组头。头文件注释因此明确指出该实现“假设小端序、依赖非对齐访问基本只适合 x86[_64] 世界”。在构建层面GroupVarint.cpp已纳入 folly/CMakeLists.txtGroupVarint.cpp/GroupVarint.h条目与 folly/BUCK作为 Folly 核心库的一部分统一编译无需额外引入依赖。七、选型建议Varint 还是 GroupVarint综合 folly/Varint.h 与 folly/GroupVarint.h 的源码注释可以给出以下务实结论单值、流式、无分组语义的编解码使用folly/Varint.h的encodeVarint/decodeVarint配合encodeZigZag/decodeZigZag处理有符号负数实现简单、接口直接批量整数数组倒排索引 posting list、ID 序列、时间戳数组优先选择 GroupVarint分组头部集中存放长度信息元数据开销平摊到整个组32 位解码在 SSSE3 平台上还有 PSHUFB 指令加持吞吐优势明显需要精确控制缓冲区上限时可用GroupVarintBase::maxSize(n)预留空间32 位每 4 值最多 17 字节64 位每 5 值最多 42 字节测试用例对此有逐值断言再用encodedSize/totalSize获取精确长度。八、进一步阅读编码格式与 API 的权威定义folly/GroupVarint.h查找表生成与掩码语义的源码注释folly/GroupVarint.cpp分组参数kGroupSize/kHeaderSize/kMaxSize与maxSize/totalSize实现folly/detail/GroupVarintDetail.h逐字节编码期望值与流式编解码行为验证folly/test/GroupVarintTest.cpp传统 base-128 Varint 对照实现folly/Varint.h本文所依据的原始文档folly/docs/GroupVarint.md【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价