std::bitset 不适合亿级去重,因其大小需编译期确定、不支持动态扩容,无法适应运行时未知范围的稀疏ID场景;而 std::vector 虽可动态位压缩,但 operator[] 返回代理引用,不支持取地址及底层内存操作。

为什么 std::bitset 不适合亿级去重?
因为 std::bitset 大小必须在编译期确定,比如 std::bitset 会直接让编译器报错或生成超大二进制;即使能编译,它也不支持动态扩容,无法应对运行时才知道数据范围的场景。真实业务中,ID 可能从 1 跳到 1e9,但实际只出现几百万个——静态位图浪费内存且不可行。
用 std::vector 手动实现动态 Bitmap 的关键点
std::vector<bool></bool> 是 C++ 标准库中唯一支持运行时大小、又做位压缩的容器,但它不是“普通容器”,而是特化实现,operator[] 返回的是代理引用,不能取地址。这意味着你不能把它当 uint8_t* 直接传给 SIMD 或 mmap 操作。
- 初始化按字节对齐:用
(max_id + 7) / 8算出所需字节数,再构造std::vector<uint8_t></uint8_t>更可控(推荐) - 设定位用
bits[index / 8] |= (1 ,注意 <code>% 8和/ 8优先级高于位移,务必加括号 - 查重用
(bits[index / 8] & (1 ,别漏判非零条件 - 避免反复计算:把
index / 8和index % 8提前存为局部变量,热点路径下有 5–10% 性能提升
内存映射(mmap)+ 自定义 Bitmap 如何支撑十亿级 ID?
当去重 ID 范围达 0~2^32−1(约 42 亿),哪怕位图只要 512MB,也建议用 mmap 加载到进程虚拟地址空间,而非 malloc。好处是:不占物理内存直到真正访问;可多进程共享同一文件;崩溃后数据不丢。
- 文件需提前
ftruncate到目标大小(例如1ULL > 3字节),否则 mmap 读写越界会触发 SIGBUS - 用
MAP_SHARED | MAP_POPULATE标志预加载页表,减少首次访问缺页中断 - 位操作仍要自己写:
uint8_t* ptr = static_cast<uint8_t>(addr); ptr[idx / 8] |= ...</uint8_t>,别依赖std::vector - 注意字节序无关性:位图本身不涉字节序,但若用
uint64_t*批量置位,需确保平台和对齐一致
查重性能卡在哪?缓存行与分支预测才是瓶颈
实测发现,单纯“位运算快”不等于整体快。当随机访问跨度超过 L3 缓存(如 ID 分布稀疏),cache miss 占耗时 70% 以上;而 if (is_set(i)) { ... } 这类分支,在 ID 存在率低时,CPU 分支预测失败率飙升。
立即学习“C++免费学习笔记(深入)”;
- 批量处理优于单点:把待查 ID 排序后分段,使
idx / 8尽量连续,提升缓存命中 - 用
__builtin_expect提示编译器:存在概率低时写if (__builtin_expect(is_set(i), 0)) - 避免 false sharing:多个线程写不同 bit,但落在同一 cache line(64 字节 = 512 bit),需手动 padding 或按 cacheline 对齐分配
- 别迷信 memset:初始化大位图时,
std::fill在 GCC 下常被自动向量化,比手写memset更稳
真正压测到每秒千万级查重时,你会发现最慢的从来不是位运算本身,而是内存布局是否亲和 CPU cache,以及你的循环有没有让编译器生成紧凑的 load-op-store 流水线。



















