Benchmark_bitset
场景
数据规模20000个,但有效数据很稀疏,50个。存的只是bool真假值。
需要做 benchmark 调研:std::vector<bool>
、boost::dynamic_bitset、std::bitset、flat_set<short>、flat_unordered_set<short>的性能差异调研。
std::bitset是固定大小的位集。
std::vector<bool>是可扩展的位集。1 个 bool 数据占 1 位。
官方宣称,如果在编译时已知位集的大小,可以使用std::bitset,它提供了更丰富的成员函数集。
boost::dynamic_bitset可以作为std::vector<bool>的替代方案。
https://www.boost.org/doc/libs/latest/libs/dynamic_bitset/dynamic_bitset.html
还需要注意:
std::vector<bool>表示形式可能经过优化,std::vector<bool>不一定满足所有 Container 或 SequenceContainer 的要求。例如,由于 std::vector<bool> 是实现定义的,因此它可能不满足 LegacyForwardIterator 的要求。使用诸如 std::search 之类需要 LegacyForwardIterator 的算法可能会导致编译时或运行时错误 。
Boost.Container 版本的 vector 并不专门针对 bool 。
总结
对稀疏真值位集(N=20,000,K≈50)在不同容器实现上的构建、查询与顺序扫描性能进行严谨对比:
std::bitset<N>boost::dynamic_bitset<>std::vector<bool>boost::container::flat_set<short>boost::unordered_flat_set<short>
骨架
先创建一个结构化的基准测试工程骨架(CMake + Google Benchmark + 示例基准项)。
先创建目录:
benchmark/bitset_bench/
- 基准框架:Google Benchmark(统计稳健性、自动热身、汇总)。
benchmark根目录的CMake文件:
1 | cmake_minimum_required(VERSION 3.16) |
子目录bitset_bench的CMake文件:
1 | set(BENCH_N 20000 CACHE STRING "Domain size (bitset length)") |
依赖
- CMake >= 3.16
C++23编译器(AppleClang/Clang/GCC)- 因为
C++23才支持flat_set
- 因为
- Google Benchmark(自动 FetchContent)
- Boost(头文件即可,用于
dynamic_bitset/unordered_flat_set)
编译选项
- 固定工作负载:默认 N=20,000,K=50,随机查询 Q=100,000(50%命中)。
- 均可通过编译期宏覆盖:
-DBENCH_N、-DBENCH_TRUE_COUNT、-DBENCH_QUERY_COUNT。
- 均可通过编译期宏覆盖:
- 编译设置:Release(
-O3 -DNDEBUG),可选 LTO(默认启用)。
Cpp程序
编译期选择 Index 类型
1 | // Use the narrowest index type that can hold [0, BENCH_N) |
这是在“编译期”选择索引类型,尽量用更窄的无符号整数来表示区间 [0, BENCH_N) 的索引,以节省内存并提升性能。
逻辑:如果 N−1 不超过 uint16_t 的上限(65535),则 IndexType 取 uint16_t;否则取 uint32_t。这完全在编译期决定,没有运行时开销。
影响:用于存放真值位置、查询序列、以及基于集合的容器键类型。N=20000 时用 uint16_t;N=200000 时用 uint32_t。
假设与边界:默认假设 N≥1;若未来 N 可能超过 2^32−1,可把分支再扩展到 uint64_t。
make_dataset 生成可复现的 K 个唯一索引(真值位置)的数据集
- 先构造
0..N-1的索引数组(reserve(domain) 是为避免反复扩容)。(参数 domain 控制) - 用固定种子
mt19937打乱顺序(保证每次运行一致)。 - 截取前 K 个(得到 K 个索引)。(参数 take 控制)
- 再
sort + unique确保“有序且唯一”。
这样返回的 ones_sorted (std::vector<IndexType>)就是升序、不重复、可复现实验的真值位置集合。随后还会用它:
complementIndices求出未命中的索引集合(全域减去已选)。- 从命中/未命中集合各采样,合成 50% 命中率的随机查询集,并再打乱顺序。
shuffle
std::shuffle(all.begin(), all.end(), rng)是 C++ 中用来随机打乱一个序列(比如数组或容器)元素顺序的标准方法。
| 代码部分 | 含义 | 说明 |
|---|---|---|
std::shuffle |
标准库中的随机重排函数 | 位于 <algorithm>头文件中 |
all.begin() |
指向序列开始位置的迭代器 | 与 all.end()一起定义了需要被打乱的范围 [begin, end) |
all.end() |
指向序列结尾(最后一个元素之后)的迭代器 | |
rng |
随机数生成器 | 一个提供了随机性的对象,是 std::shuffle随机性的来源 |
| 如果希望每次程序运行时打乱的顺序都相同(例如,为了复现某个测试结果),可以为随机数生成器提供一个固定种子: |
1 | std::mt19937 rng(42); // 使用固定种子 42 |
- 区别于已弃用的函数:在 C11 之前,人们使用
std::random_shuffle,但它默认依赖rand()函数,随机性较差且非线程安全。自 C14 起它被弃用,C17 中已被移除。**std::shuffle是现代 C 推荐的方式**。 - 容器要求:
std::shuffle要求容器支持随机访问迭代器,因此它适用于std::vector、std::array、std::deque和普通数组等。但不适用于std::list这类不支持随机访问的容器。
技巧:排序-压缩-删除
C++中一个非常经典的STL操作,用于从容器中删除所有重复元素,只保留唯一值。它通常被称为“排序-去重”或“unique-erase”惯用法。
下面这个表格可以帮你快速理解整个操作过程:
| 步骤 | 函数 | 作用 | 说明 |
|---|---|---|---|
| 1. 排序 | std::sort(all.begin(), all.end()) |
使重复元素相邻 | (关键前提) 这行代码本身不包含排序,但你必须先做这一步,否则去重无效 |
| 2. 压缩 | std::unique(all.begin(), all.end()) |
将相邻重复元素移到末尾 | 返回一个指向新逻辑末尾的迭代器 |
| 3. 删除 | all.erase(new_end, all.end()) |
物理删除末尾的重复元素 | 容器大小真正变小 |
代码
1 | std::vector<IndexType> generateUniqueIndices(std::size_t domain, |
Benchmark项目
- 从0到20000构建
- random query,一半命中,一半未命中。测试contains性能
- 顺序扫描
总代码
1 |
|
矩阵模板
1 | impl,N,K,Q,benchmark,min(ns),median(ns),mean(ns),stddev(ns),notes |
测试执行
1 | rm -rf ~/Work/benchmark/build |
-S:用于指定源代码目录,即顶级 CMakeLists.txt文件所在的目录。例如,-S .表示使用当前目录作为源码目录
。
-B:用于指定构建目录,即所有中间构建文件(如 Makefile、CMakeCache.txt)和最终编译输出文件存放的目录。如果指定的目录不存在,CMake 会自动创建它。
最常见的用法是 cmake -S . -B build。这个命令的含义是:使用当前目录(.)的源码,并在当前目录下创建一个名为 build的文件夹来存放所有构建文件
除了 -S和 -B,CMake 还提供了许多其他实用选项。下表进行了归纳:
| 选项 | 说明 | 示例 |
|---|---|---|
-D <var>=<value> |
设置或覆盖 CMake 缓存变量。这是最常用的选项之一,用于传递配置参数。 | -DCMAKE_BUILD_TYPE=Release |
-G <generator-name> |
指定生成器,即选择使用哪种底层构建系统(如 Makefile 或 Ninja)。 | -G "Ninja" |
-j [<jobs>] |
在运行 cmake --build时使用,指定并行编译的作业数,以加快构建速度。 |
cmake --build build -j 8 |
--target <tgt> |
在运行 cmake --build时使用,指定要构建的具体目标(如某个可执行文件或库)。 |
cmake --build build --target m |
生成compile_commands.json并在工作区创建符号链接
确保 IDE 能正确解析头文件与跳转。
在 benchmark/CMakeLists.txt中启用CMAKE_EXPORT_COMPILE_COMMANDS。
然后,重新运行CMake。
1 | cmake -S . -B ./build -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON |
之后,在Work根目录创建符号链接:
1 | ln -sf /Users/mrcan/Work/benchmark/build/compile_commands.json /Users/mrcan/Work/compile_commands.json |
作用:
- 在 benchmark/CMakeLists.txt 增加了:
- set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
- 重新生成了编译数据库:
- 生成位置:
/Users/mrcan/Work/benchmark/build/compile_commands.json - 在工作区根建立符号链接:
/Users/mrcan/Work/compile_commands.json
- 生成位置:
- C++ 标准与依赖:
- 已设置
-std=c++20,并通过benchmark::benchmark目标自动注入-I.../benchmark-src/include搜索路径。
- 已设置