std::hive 是 C++26 标准库里新加入的容器,核心卖点一句话就能说完:插入和删除是常数时间,同时迭代器、引用和指针不会因为其他元素的增删而失效。这两个能力在std::vector上很难同时成立,在std::list上虽然成立但迭代又慢。所以社区里最常见的问题就是:std::hive 到底有多快,值不值得在实体管理、ECS、观察者列表、事件订阅这类场景里替换现有容器?
这篇就从实际验证的角度来拆:先讲清楚它解决什么问题,再讲怎么搭一个公平的基准测试,然后聊决定性能的参数和实现细节,最后给一份使用边界和踩坑清单。我的建议很简单:不要只看一遍 API 就动手替换,先按下面的流程跑一轮小样本测试,再决定要不要落地。
1. 先看它解决的是哪一个“不可能三角”
1.1 为什么 vector 和 list 都不能同时满足需求
如果你维护一个对象集合,绝大多数业务场景其实只有三种需求:频繁遍历全部元素、随机或按条件删除元素、在遍历过程中新增元素。这三个需求单独拿出来都很简单,但组合起来会暴露容器的短板。
std::vector的优势是内存连续、缓存命中率高、支持随机访问,遍历速度几乎是标准容器里最快的。可它的问题也很明显:在中间erase或insert需要移动后续所有元素,复杂度是 O(n)。更难受的是,vector 扩容时会重新分配整块内存,所有迭代器、引用、指针全部失效。哪怕只是删除中间一个元素,后面的迭代器也会受到扰动。
std::list采用节点分配,插入和删除单节点都是 O(1),迭代器也稳定。但它的每个节点单独分配在小块内存上,遍历时需要沿着指针一个个跳,缓存命中率差,速度通常明显慢于 vector。节点本身也有额外的指针开销,内存占用并不低。
所以当一个场景同时需要“频繁扫描所有对象”和“在扫描过程中删除对象”时,vector 删除代价太大,list 遍历代价太大。这正是 std::hive 想填的空档。
1.2 hive 的存储方式:分组加跳过
std::hive 的基本思路是把元素放进一系列“块”里。每个块内部是一段连续的元素存储区,同时维护用于标记“哪些槽位还活着”的跳过信息。删除元素时,对象并不会被逐个移动,只把对应槽位标记成空闲;遍历时,迭代器利用跳过信息快速越过死槽位,而不是一个一个检查。后续插入时,新元素可以复用这些空闲槽位。
这个设计和 list 最大的区别是:块内部是连续内存,遍历时不会完全退化成指针跳跃。和 vector 最大的区别是:元素一旦被创建,地址就不会变,新增或删除其他元素都不会让已有对象的位置发生移动。
这样带来的结果是:插入和删除近似常数时间,迭代器、引用和指针稳定,遍历速度虽然不一定追得上 vector 的纯连续扫描,但通常明显好于 list。
1.3 和常见容器的定位差异
| 容器 | 中间插入/删除 | 增删其他元素后迭代器稳定性 | 遍历速度 | 随机访问 | 典型取舍 |
|---|---|---|---|---|---|
| std::vector | 中间操作 O(n) | 扩容后失效,中间操作后后续迭代器受影响 | 最快 | 支持 | 删除贵,缓存好 |
| std::list | 单节点操作 O(1) | 稳定 | 较慢,缓存不友好 | 不支持 | 插入删除便宜,遍历贵 |
| std::deque | 中间操作 O(n),视实现而定 | 不稳定 | 较快 | 支持 | 两端操作便宜,中间贵 |
| std::hive | 摊还 O(1) | 稳定 | 快,跳过空洞的块内扫描 | 不支持 | 换取了稳定引用和删除效率 |
我一般建议用一句话判断:如果你的代码里“每帧或每个循环都要扫描全部对象,同时不断有对象被删除和新增”,那 hive 就是从容器设计上更贴合这个负载的方案。
2. 实测前先确认环境和测试口径
2.1 确认编译器是否支持
std::hive 虽然已经进入 C++26 工作草案,但不同编译器的标准库实现进度差异很大。有些编译器需要开启 C++26 或 latest 模式,有些目前还没有实现。你没必要卡在“必须等编译器支持”上,因为 std::hive 的前身 plf::hive 是单头文件库,把 plf 的头文件放进工程,命名空间换成 plf,API 基本一致。
这里的思路是:先用 plf::hive 验证性能趋势和业务逻辑,等本机标准库支持成熟后再切到 std::hive。两种实现的容器结构和复杂度性质一致,得出的结论基本能迁移。
# GCC/Clang 示例,具体参数以编译器版本为准 g++ -std=c++26 -O2 bench.cpp -o bench # MSVC 示例 cl /std:c++latest /O2 bench.cpp如果编译器没有 ,就下载 plf::hive 的单头文件,包含进工程,把std::hive替换成plf::hive。
2.2 为什么测试口径比测试本身更重要
容器基准测试最容易犯的错误不是代码写错,而是负载设计失真。比如只测“纯遍历”,那 vector 几乎一定能赢;只测“往 begin 位置插入”,那 vector 会被 hive 甩开很多。这两种都不是真实业务。
更稳的做法是先把业务负载拆成三个维度:
- 容器最大规模:1000、10 万、100 万个元素,结论可能不同。
- 删除比例:每轮删除 1%、10%、50%,删除越多 vector 移动成本越高。
- 遍历次数:每轮遍历一次还是一百次,决定缓存命中率的影响权重。
我会先按这三个维度各跑一组,而不是只跑一个固定规模。只有这样,你才能说清楚“在什么条件下 hive 更快,在什么条件下不如 vector”。
2.3 注意版本和优化开关
基准测试一定要在 Release、开启优化的情况下跑。Debug 模式下 MSVC 的迭代器检查会大量增加容器操作开销,hive 这种需要精细指针操作的容器受影响尤其明显。另外,关闭其他占用 CPU 的应用,多轮取中位数,避免只取一轮时间。
3. 手写一个最小基准测试
3.1 先测纯遍历
最小测试能回答一个问题:在完全没有删除的情况下,hive 比 vector 慢多少。真实场景里这不是最终结论,但它能帮你评估 hive 的遍历底噪。
#include <hive> // 编译器支持时;否则用 plf::hive #include <vector> #include <chrono> const int N = 1'000'000; const int ROUNDS = 20; volatile long long sink = 0; template <typename F> long long measure_ms(F&& f) { auto t0 = std::chrono::steady_clock::now(); f(); auto t1 = std::chrono::steady_clock::now(); return std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count(); }填充数据时,对 vector 可以用push_back,对 hive 用insert或emplace:
std::vector<int> v; for (int i = 0; i < N; ++i) v.push_back(i); std::hive<int> h; for (int i = 0; i < N; ++i) h.insert(i);遍历部分要防止编译器把空循环优化掉,所以加一个累加变量:
long long sum = 0; for (int r = 0; r < ROUNDS; ++r) { for (auto x : h) sum += x; } sink = sum;这个测试只说明一件事:hive 遍历所有存活元素的开销大约在什么水平。通常结果会是 vector 最快,hive 次之,list 明显最慢。如果 hive 比 list 还慢,那首先要怀疑是不是实现或编译模式有问题。
3.2 再测“扫描过程中删除”
这是 hive 最值得测的场景,也是它设计之初就瞄准的场景:遍历容器,删除满足条件的元素,过程中还有新增。
auto it = h.begin(); while (it != h.end()) { if (*it % 3 == 0) { it = h.erase(it); // hive 返回下一个有效迭代器 } else { ++it; } }对 vector 要做同样语义的删除,但要用更合适的写法:
v.erase( std::remove_if(v.begin(), v.end(), [](int x) { return x % 3 == 0; }), v.end());这里有个很容易踩的误区:如果给 vector 也写一个循环里erase的版本,性能会非常差,但这种差不是 vector 的公平水平。要比较,就必须让每个容器使用自己的最佳实践。vector 的最佳实践是 erase-remove,hive 的最佳实践就是直接遍历删除。
如果删除比例高,比如每轮删掉一半元素,vector 需要不断移动大量元素,hive 则只标记槽位,差距会很明显。如果删除比例只有 1%,vector 的移动成本分摊下来不高,hive 的块元数据开销反而可能让优势变弱。
3.3 再测中间插入和反复重建
第三个测试是反复插入删除。比如每轮随机在中间位置插入一批元素,再删掉一部分。
vector 在中间插入会移动后续元素,O(n) 成本跑不掉;hive 插入时只要找到空闲槽位,成本近似常数。这里要注意的是,hive 没有随机访问,随机“中间位置”要依赖迭代器前进,这个前进动作本身是 O(n),但通常遍历插入的场景总是从 begin 到 end 扫描,此时插入自身的常数时间优势能发挥出来。
如果你的业务是“每次定位到某个头部或中部索引再插入”,那这份随机定位成本已经抵消了 hive 的插入优势,这种情况下 vector 可能更合适。
3.4 结果怎么判断
我的判断经验是这样:
- 纯遍历且无删除:vector 赢,hive 在可接受范围,list 输。
- 遍历中删除,删除比例不低:hive 赢,删除比例越高优势越明显。
- 频繁中间插入且插入点通过扫描到达:hive 赢。
- 需要按索引随机访问:hive 根本不参与比较,硬件上不是你该用的容器。
- 容器规模很小,比如只有几十个元素:直接用 vector,hive 的块管理开销没有意义。
不要拿一次测试结果当唯一结论。不同元素大小、不同删除比例、不同初始化方式,结果会变。关键是理解趋势,而不是记住某一个环境里的具体倍数。
4. 影响 std::hive 性能的几个关键点
4.1 块的大小影响缓存和空洞
std::hive 的实现会把元素放进块里,块内部是连续存储。标准并没有硬性规定块的大小和调整策略,plf::hive 的构造参数里可以通过用户传入 block size 来调节。
块越大,遍历时连续读入的内存越多,缓存友好度越高;但代价是块内可能留下更多空闲槽位,删除比例高时内存浪费更多。块越小,插入和空间复用更灵活,但块与块之间的跳转更频繁,遍历时连续感下降。
一般默认值适合绝大多数普通对象。如果你的元素特别小,比如 int 这种,可以试试调大块;如果元素特别大,比如几百字节的结构体,块内能放的元素数量本来就不多,调小一点反而更灵活。建议在测试时单独跑一组不同 block size 的对比。
4.2 迭代器类别决定了不能做什么
std::hive 的迭代器不是随机访问,不支持operator[]和按索引跳到中间。如果你调用std::distance计算两个迭代器之间的距离,因为迭代器是双向的,这个操作复杂度是 O(n),不是 O(1)。
这个是设计取舍:为了稳定引用和跳过空洞,它放弃了随机访问能力。所以如果算法里大量依赖“根据编号找到第 k 个对象”,hive 不合适;如果只是按顺序扫描全部对象,hive 没有任何问题。
4.3 稳定引用的代价是空间和管理
hive 保证不移动已有元素,所以插入时不需要把旧元素搬到新内存。但从另一个角度看,这种稳定性是靠块内槽位复用和跳过信息换来的。也就是说,只要元素插入过并被删除,那块内存区域就可能保留空洞,直到新的元素填进来。
如果容器的生命周期是“先填满,再全部清空”,vector 的紧凑存储优势明显。如果容器长期处于“不断新增、不断删除、保持在某个规模附近”的状态,hive 的空间复用能发挥价值。
4.4 内存开销怎么看
从我实际观察来看,hive 的内存开销通常低于 list,因为 list 每个节点都有额外的指针和分配头;但高于 vector,因为每个块有元数据,块内也可能有空闲槽位。
真要评估内存,不要只看 sizeof,要看运行时的整体占用。建议在测试程序里打印容器的容量或记录系统级峰值内存。如果内存接近上限,先用小规模数据确认 hive 的空洞率,再决定是否替换。
4.5 和其他哈希容器组合使用
有时候最优解不是“整个项目只用一个容器”。常驻的、几乎不删除的核心数据可以继续放 vector;短期存活的动态对象,比如子弹、特效、临时订阅者,可以放进 hive。两者配合,比强行用 hive 保存所有数据更稳妥。
5. 什么时候用 std::hive,什么时候不要用
5.1 适合的场景
我在实战里看到 hive 最典型的用武之地是三块:
一是游戏实体管理。每帧遍历所有实体做更新,战斗过程中频繁出生子弹、怪物,删除死亡对象。vector 在高压删除时性能不稳,list 遍历又太慢,hive 正好匹配“遍历 + 增删”并存的负载。
二是 ECS 或组件对象管理。组件对象经常被外部持有引用,如果容器扩容导致对象移动,引用就失效了。hive 能保证引用稳定,插入删除也不贵。
三是观察者列表、事件订阅表、回调注册表。这类容器经常在遍历过程中出现“回调内部把自己从列表移除”的情况,hive 遍历时删除当前元素是安全且便宜的。
5.2 不适合的场景
也有几类场景建议不要盲目替换。
需要按索引访问的场景,比如“根据 id 取第 k 个元素”,vector 或 unordered_map 更合适。需要严格保持插入顺序并且经常按顺序输出的场景,hive 的迭代顺序不做严格保证,删除空洞后顺序会变化。容器规模很小,几十个元素,直接 vector,连讨论性能的必要都没有。
另外,如果对象生命周期极长、从不删除,hive 的块管理开销就是纯浪费。vector 的紧凑数组一定更省更高效。
5.3 渐进式替换路径
不要一次性把所有容器都换掉。我建议按四步走:
- 先在一个小模块里引入 hive,用真实数据结构代替原来的 vector 或 list。
- 保留旧实现,在代码里通过别名或宏切换,方便随时回退。
- 跑原有功能测试和压力测试,确认行为差异,特别是迭代顺序是否有影响。
- 在真实负载下记录遍历时间、删除时间、内存峰值,再和旧实现对比。
最快的验证方式是把容器类型做成一个别名,比如using EntityContainer = std::hive<Entity>;,后续切换回 vector 只需要改一行。
6. 踩坑记录:哪些问题最容易出现
6.1 迭代顺序不是插入顺序
hive 遍历时只会跳过死槽位并访问存活元素,但不会保证元素按插入顺序输出。删除元素后,新插入的元素可能会复用被删除的位置,导致遍历顺序变化。
如果业务逻辑依赖“先进先出”或“按创建时间遍历”,就要先评估这个影响。可能需要额外维护时间戳或顺序编号,这部分成本有时会抵消 hive 的收益。
6.2 erase 之后不要继续使用旧迭代器
在循环里删除元素,正确写法是接收erase的返回值,比如it = h.erase(it);。删除之后,再对旧迭代器解引用是未定义行为,这点和 list 类似。注意,不能把erase返回的迭代器再往下跳一步,否则会跳过元素。
使用范围 for 循环时,不要在循环体内删除当前元素,因为范围 for 隐藏了迭代器推进逻辑,清理起来容易出错。需要边遍历边删除就老老实实写 while 循环。
6.3 找不到头文件
这个问题最常见,也最好解决。
先确认编译器是否真的支持 C++26 标准库里的 hive。如果支持,需要开启对应的语言标准选项。如果不支持,直接用 plf::hive 的单头文件版本。有一点要注意:std::hive和plf::hive是两套名字,不要混着包含。你可以在代码里做一个小封装,统一对外只暴露using别名,避免到处散落命名空间判断。
6.4 性能数据波动大
如果测试结果来回差好几倍,优先检查几件事:
- 是不是没开优化,尤其 MSVC Debug 下的迭代器检查。
- 是不是测试里用了未初始化的数据,导致每次内存分配行为不同。
- 是不是有其他进程抢占 CPU,或者笔记本在省电模式。
- 是不是编译优化把空的累加循环直接消除了。
建议每一轮测试执行多次,取中位数而不是最小值。最小值容易受到系统噪声干扰,中位数更稳。
6.5 别拿 hive 和 vector 在“只插不删”场景比较
如果你把 hive 用在只 push 不删除的场景,hive 是吃亏的。它要维护块结构、跳过信息、空闲槽位,这些都需要一点成本。这种场景 vector 就是最合适的。hive 的优势必须放在“有删除、有插入、有遍历”的组合负载里才能体现。
真正判断一个容器选得对不对,不是看单次操作有多快,而是看一个月后代码里新增需求时,这个容器还撑不撑得住。
我个人更建议先在小模块里用 plf::hive 跑通逻辑,再把负载测试补上,不要一上来就把核心容器替换掉。std::hive 的真正价值在于它能长期保持稳定引用,同时把扫描和删除两种操作都维持在可接受水平。至于它能不能在你的工程里跑出优势,还是要靠你自己的数据和环境来回答。