尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C++容器性能对比:plf::hive vs std::list与std::vector实测

C++容器性能对比:plf::hive vs std::list与std::vector实测 先说明一个容易踩坑的事实C26 标准草案里目前并没有官方名称为std::hive的容器。标题里的std:hive更像是对社区中plf::hive容器库以及相关标准提案讨论的简称。在 C 标准演进的过程中确实有把类似hive的容器纳入标准库的讨论但在写这篇文章时它还不是 C26 的正式成员。那为什么这个问题值得聊因为plf::hive是一种号称“兼具链表插入删除灵活性、又比链表快很多”的容器。它不保证元素在内存中完全连续但通过分块分配和双向链表式的迭代能在频繁插入/删除场景下保持很好的缓存局部性。很多人在问“它到底比std::list快多少”“能不能平替std::vector”“迭代器会不会失效”这篇文章直接围绕这几个问题展开。全文会做四件事说清楚std::hive/plf::hive的真实身份和设计目标。给出一套可在本机复现的基准测试工程对比std::vector、std::list和plf::hive。分析插入、遍历、删除、内存占用四个维度的性能差异。给出选型建议什么场景值得用它什么场景不要碰。1. 核心能力速览能力项说明项目类型高性能 C 容器库常见名称plf::hive社区常简称为 hive 容器标准状态不属于 C26 正式标准属于第三方开源库/提案讨论对象主要特性稳定的迭代器、指针引用不失效、快速插入删除、块状内存分配时间复杂度插入 O(1)删除 O(1)遍历近似 O(n)随机访问不支持operator[]不适合按下标查找头文件形态单头文件仅需 include 头文件许可证Zlib 许可以实际仓库 LICENSE 为准编译要求C11 及以上较新版本建议 C17/20适合场景高频插入删除、迭代器稳定、节点生命周期较长的场景不适合场景需要随机访问、排序、二分查找的场景从这几行规格就能看出这个容器的定位不是“替代std::vector”而是“改善std::list在缓存友好性上的缺陷”。2. 适用场景与使用边界先讲清楚它能解决什么问题。在很多业务代码里我们需要一个容器既要支持频繁的新增和删除又要保证“已经被拿到的指针/引用/迭代器”不会因为其他元素被删除而失效。std::vector做不到因为它扩容时会整体搬移元素std::list虽然能做到但每个节点独立分配遍历时内存跳跃严重缓存命中率低。plf::hive的思路是按固定块大小分配内存块内部连续存储元素通过空闲链表管理空位。插入和删除只影响局部节点不会触发整块移动。这样一来它适合这几类场景游戏引擎中的实体管理大量对象频繁生成和销毁但对象之间彼此持有指针。网络连接管理连接不断建立、断开需要稳定的连接对象标识。事件系统、观察者列表注册和注销频繁遍历回调时又不希望迭代器失效。自研内存池场景的补充不想自己写空闲链表想用现成容器。使用边界也要说清楚不要拿它当数组用。没有operator[]不支持下标访问和随机跳转。标准算法中的std::sort、std::binary_search传入它的迭代器大多数情况下没法直接工作。它的内存占用一般高于std::vector因为块分配和空闲列表会有点开销。跨平台使用时需要确认编译器版本如果项目强制要求标准库实现那它暂时不适用因为它不是标准库的一部分。另外性能类结论很容易受到“某个编译器版本、某次 benchmark 写法”影响。任何评测文章给出的数字都只能作为参考真正要不要引入项目必须在自己目标编译器和 CPU 上复测。3. 环境准备与基准测试设计3.1 软硬件环境先准备好测试环境建议至少具备以下条件项目建议操作系统LinuxUbuntu 22.04、Windows 或 macOS 均可编译器GCC 11 / Clang 14 / MSVC 2022标准版本C17 或 C20依赖库仅需要plf::hive头文件构建工具CMake 3.16 或直接命令行编译CPUx86_64 或 Apple Silicon 均可内存不需要特殊要求但建议关闭其他大型程序避免干扰这里不推荐打开编译器自动向量化之外的激进优化参数做“竞赛式测试”因为生产环境通常不会开-marchnative之外的特殊指令集。3.2 获取 plf::hive 头文件plf::hive是单头文件库从 GitHub 仓库获取plf/hive.h即可。它的 license 是 zlib 风格可以自由用于开源和商业项目但要注意保留版权声明。假设目录结构如下hive_bench/ ├── CMakeLists.txt ├── main.cpp └── third_party/ └── plf/ └── hive.h如果只是临时测试可以直接用命令行编译。# 假设 hive.h 在 third_party 目录下 g -stdc17 -O2 -I./third_party main.cpp -o hive_bench3.3 基准测试的通用注意事项C 容器基准测试很容易写出“看似合理、实际误导”的代码。需要特别注意这几点禁止编译器优化掉整个循环对计算结果做volatile累加或调用一个外部不可内联函数防止空循环被优化。随机插入删除要保证公平不能只用 0 到 10 之间的随机数导致不同容器访问模式差异过大。预热后再计时第一次运行会触发内存分配和页表映射产生噪音。多次运行取中位数单次运行结果受 CPU 频率和后台进程影响大。4. 安装部署与编译运行4.1 命令行编译使用以下命令可以直接编译运行g -stdc17 -O2 -I./third_party main.cpp -o hive_bench ./hive_bench如果需要 Debug 版本检查迭代器逻辑和断言可以去掉-O2并加上-DPLF_HIVE_DEBUG如果库支持该宏具体以官方文档为准。4.2 使用 CMake 构建推荐使用 CMake 管理方便后续继续加 benchmark 依赖。cmake_minimum_required(VERSION 3.16) project(hive_bench LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(hive_bench main.cpp) target_include_directories(hive_bench PRIVATE ${CMAKE_SOURCE_DIR}/third_party) target_compile_options(hive_bench PRIVATE -O2)然后执行mkdir build cd build cmake .. cmake --build . ./hive_bench4.3 验证头文件是否正确引入在main.cpp中写一个最基础的启动验证#include cstddef #include iostream #include plf/hive.h int main() { plf::hiveint h; h.insert(1); h.insert(2); h.insert(3); std::cout hive size: h.size() std::endl; for (auto it h.begin(); it ! h.end(); it) { std::cout *it ; } std::cout std::endl; return 0; }如果编译和运行都正常说明环境已经就绪。接下来可以开始正式的基准测试。5. 功能测试与性能对比5.1 插入性能测试插入性能是hive的核心卖点之一。下面用一个可控的测试来观察它和std::vector、std::list的差异预先插入 N 个整数。在迭代过程中每隔固定步长插入新元素。避免使用push_back这种对std::vector特别友好的连续追加模式。#include chrono #include deque #include iostream #include list #include vector #include plf/hive.h using Clock std::chrono::steady_clock; template typename Container void bench_insert(Container c, int initial_count, int insert_times, int step) { auto start Clock::now(); // 先填充初始数据 for (int i 0; i initial_count; i) { c.insert(c.end(), i); } // 在遍历过程中插入 int counter 0; auto it c.begin(); while (it ! c.end()) { if (counter % step 0 insert_times 0) { it c.insert(it, -counter); insert_times; // 注意插入后 iterator 语义取决于具体容器 // hive 和 list 插入后迭代器不失效vector 会失效这里不做统一语义 break; } counter; it; } auto end Clock::now(); std::cout elapsed: std::chrono::duration_caststd::chrono::milliseconds(end - start).count() ms std::endl; }这种写法其实暴露了一个问题不同容器的insert语义不一样很难写出“完全等价的操作序列”。所以更合理的做法是分开测尾部追加虽然会触发vector扩容但这本来就是vector的强项。头部/中间插入这是hive和list的强项。重复插入与删除交替模拟高频碎片化操作。一个比较合理的基准测试代码如下#include chrono #include cstdlib #include iostream #include list #include vector #include plf/hive.h template typename Container double test_mixed_ops(int n) { Container c; auto start std::chrono::steady_clock::now(); for (int i 0; i n; i) { c.insert(c.end(), i); } // 随机删除一半元素 for (int i 0; i n / 2; i) { int target std::rand() % static_castint(c.size()); auto it c.begin(); std::advance(it, target); c.erase(it); } // 再插入一批 for (int i 0; i n / 2; i) { c.insert(c.end(), i); } auto end std::chrono::steady_clock::now(); return std::chrono::durationdouble, std::milli(end - start).count(); } int main() { const int n 100000; double vec_time test_mixed_opsstd::vectorint(n); double list_time test_mixed_opsstd::listint(n); double hive_time test_mixed_opsplf::hiveint(n); std::cout vector: vec_time ms\n; std::cout list: list_time ms\n; std::cout hive: hive_time ms\n; return 0; }这段代码的问题在于std::vector在随机删除时需要 O(n) 搬移元素和list、hive的 O(1) 删除完全不是一个公平比较的对象。因此它更适合用来观察“真实业务里哪种操作更划算”而不是“谁的删除操作更高效”。需要特别说明这类测试的运行结果受编译器和容器实现影响很大不能直接把别人的数字当成结论。在自己的机器上跑完后重点看相对趋势而不是绝对值。5.2 遍历性能测试遍历是hive和std::list差距最大的地方。#include chrono #include cstdint #include list #include vector #include plf/hive.h template typename Container std::int64_t bench_iterate(Container c, int iterations) { volatile std::int64_t sink 0; auto start std::chrono::steady_clock::now(); for (int round 0; round iterations; round) { for (auto it c.begin(); it ! c.end(); it) { sink *it; } } auto end std::chrono::steady_clock::now(); (void)sink; return std::chrono::duration_caststd::chrono::milliseconds(end - start).count(); } template typename T void fill_container(T c, int n) { for (int i 0; i n; i) { c.insert(c.end(), i); } } int main() { const int n 1000000; std::vectorint v; std::listint l; plf::hiveint h; v.reserve(n); fill_container(v, n); fill_container(l, n); fill_container(h, n); std::cout vector iterate: bench_iterate(v, 10) ms\n; std::cout list iterate: bench_iterate(l, 10) ms\n; std::cout hive iterate: bench_iterate(h, 10) ms\n; return 0; }这段代码的关键点在于plf::hiveint的遍历。由于hive内部按块分配内存块与块之间不连续但块内元素相对连续。相比std::list每个节点单独分配hive的缓存命中率要高很多但通常仍然比不上std::vector的完全连续内存遍历速度。从实际经验看遍历性能通常呈现vector hive list。但差距有多大取决于元素类型大小、块大小、CPU 缓存策略。所以这篇文章不给出固定数字而是建议你通过这段代码在自己机器上确认。5.3 删除与迭代器稳定性测试hive最强的特性不是“删除快”而是“删除元素时其他元素的迭代器不失效”。标准库中的std::list也能保证这一点但std::vector在中间删除后会移动大量元素。这里给一个验证迭代器稳定性的测试#include cassert #include iostream #include plf/hive.h struct Item { int value; int token; }; int main() { plf::hiveItem h; auto it1 h.insert({100, 1}); auto it2 h.insert({200, 2}); auto it3 h.insert({300, 3}); // 删除中间元素 it2 h.erase(it2); // it1 和 it3 仍然有效 assert(it1-value 100); assert(it3-value 300); std::cout it1 value: it1-value std::endl; std::cout it3 value: it3-value std::endl; return 0; }如果这段代码安全运行说明当前版本的hive确实保证了“非删除元素的迭代器稳定性”。这个特性在很多业务场景中比“速度快”更有价值因为它能极大简化代码逻辑不需要自己维护一层索引。6. hive 容器接口 API 一览plf::hive的接口设计尽量向标准容器靠拢但又不完全一致。下面是常见接口接口作用注意事项insert(value)插入单个元素返回迭代器性能近似 O(1)emplace(args...)原地构造元素避免移动开销erase(iterator)删除元素只使被删除元素的迭代器失效erase(iterator_first, iterator_last)删除区间元素区间语义和标准容器类似clear()清空容器释放元素但不一定释放所有块size()返回元素数量size_t类型empty()判断是否为空C11 后为 O(1)begin()/end()迭代器访问支持正向迭代非随机访问cbegin()/cend()const 迭代器遍历用max_size()最大容量平台相关shrink_to_fit()缩容将空闲块释放回分配器但不是所有实现都有此方法reserve(count)预分配块数效果和 vector 不同不会分配连续内存get_block_capacity()获取块容量用于内存调优get_block_count()获取块数反映当前分配了多少块使用hive时最容易犯的错误是试图对迭代器做随机移动比如it 100这在std::list上同样不可行。需要随机访问时应该考虑其他容器。7. 资源占用与缓存局部性观察hive不是“纯链式”结构也不是“纯连续”结构。它的内存分配策略是以块为单位块内连续存放元素。这样每次遍历到同一个块内部的元素时CPU 缓存能发挥较好效果跨块时则需要跳转。观察资源占用可以从这几个角度入手内存占用比较std::vector、std::list、plf::hive在插入相同数量 int 时malloc分配的总字节数。vector通常最省list因为每个节点独立分配会有额外开销hive介于两者之间。块数量可以通过get_block_count()观察。元素数量少时块数少元素数量多时块数增多但不会像list那样一个元素一个节点。删除空洞删除元素后块内产生空洞但后续插入会优先复用空洞。这能减少内存分配次数但如果删除后长时间不插入空闲块不会完全消失内存占用可能偏高。想测试分配器的真实行为可以把plf::hive的默认分配器替换成自定义计数分配器统计分配次数和释放次数。标准容器也可以用同样的手段做对比。8. 常见问题与排查方法问题现象可能原因排查方式解决方案编译报错找不到plf/hive.hinclude 路径没配置检查-I参数或 CMake target_include_directories把头文件目录加入搜索路径编译要求 C11 但报语法错误编译器太老查看编译器版本升级 GCC/Clang/MSVChive遍历结果顺序和插入顺序不一致容器不保证稳定遍历顺序阅读官方文档确认顺序语义需要保序时改用其他容器erase后迭代器遍历崩溃在遍历中删除当前迭代器但继续使用检查遍历代码删除后立即把迭代器赋值为下一个有效位置内存占用高于预期空闲块未释放或元素类型过大使用get_block_count()观察块数调用shrink_to_fit()或定期重建容器std::sort无法编译hive迭代器不满足随机访问迭代器要求查看错误信息中迭代器 category拷贝到 vector 再排序性能基准测试结果波动大后台进程或 CPU 频率变化增加预热和多次运行取中位数关闭后台程序绑定 CPU 核心运行与现有代码冲突命名空间plf冲突查看编译错误使用命名空间别名或自定义宏如果库支持线程并发读写hive本身非线程安全多线程场景需外部加锁不同线程使用不同实例或加并发保护排查这些问题时建议先写一个最小复现程序不要直接在大项目里改。因为hive的迭代器语义和vector差异较大很多崩溃其实是使用方式不对。9. 最佳实践与使用建议9.1 什么场景优先考虑 hive对象生命周期碎片化严重创建和销毁非常频繁。对象之间有指针或引用关系且不希望因为容器扩容导致悬空引用。遍历是主要操作但偶尔要删除中间元素。对缓存命中率要求高又不想引入复杂的内存池。9.2 什么场景不要用 hive需要按下标访问比如container[i]。需要排序、二分查找、std::nth_element这类随机访问算法。容器元素数量极少且固定不变直接std::array或std::vector更清晰。需要严格的插入顺序保证尤其要求遍历顺序等于插入顺序。hive不提供这种保证因为删除后空洞复用会让顺序不确定。9.3 工程化建议第一引入前先写 baseline。保留当前容器的性能压测代码再实现一份hive版本用同一组数据和同一台机器对比。没有 baseline 的结论没有说服力。第二把容器类型抽象成别名而不是到处写plf::hiveItem。这样后续如果标准库推出了类似容器可以低成本迁移。// aliases.hpp #include plf/hive.h template typename T using DynamicSet plf::hiveT;第三关注元素类型大小。如果元素很小比如inthive的优势不如元素是大型结构体时明显如果元素本身很大内存分配开销会被构造/析构开销掩盖。第四测试要覆盖碎片化场景。只做顺序插入、顺序遍历、顺序删除无法体现hive的价值。真实业务里的插入删除往往是随机的所以基准测试要模拟这类访问模式。第五不要为了“新”而引入。第三方库意味着维护成本、ABI 兼容风险和团队学习成本。只有性能收益明显大于维护成本时才值得替换。10. 性能结论到底快在哪回到标题的问题hive到底有多快从设计上看它有三个核心优势插入和删除近似 O(1)且不会导致其他元素的内存搬移。缓存局部性优于std::list遍历性能通常明显更高。迭代器/指针/引用稳定性强减少业务代码里对“失效”的防御逻辑。但这不代表它比std::vector快。在尾部追加、随机访问、批量初始化这些场景里std::vector依然是更优选择。真正的 Fast 是相对的在频繁插入删除且需要稳定引用的场景下hive 是比 list 更快的选择是比 vector 更省心的选择。如果想验证建议在自己环境下跑这样一组最小对比插入 100 万次。遍历 10 轮。随机删除 50 万次。再遍历 10 轮。比较std::list和plf::hive的耗时和内存变化很容易看到差距。11. C26 会正式引入 std::hive 吗从标准演进的角度看C26 的标准库容器重点在std::flat_map、std::flat_set、std::inplace_vector等新容器上std::hive并不是已经被投票通过的正式标准单元。不过hive的思路已经引起过 WG21 讨论类似容器完全有可能在未来某个 C 版本进入标准库。对开发者来说这反而是一种值得关注的方向。第三方库能把容器做到这种程度说明标准库容器并不是性能终点。在正式标准支持之前plf::hive可以作为备选方案先落地。12. 总结与评估plf::hive是一个设计目标清晰、实现质量较高的高性能容器库。它不是std::vector的替代品而是std::list在“迭代器稳定性 遍历性能”两个方向上的重要改进。最值得先做的验证是在自己的业务场景中构造一组高频插入删除的测试数据对比hive和当前容器的运行时间、内存峰值、代码改动成本。如果之前为了规避迭代器失效问题写过大量防御性拷贝或索引维护代码hive带来的简化可能比性能提升更有价值。最容易踩的坑是把它当成“万能容器”试图用它替代所有std::list和std::deque。实际使用中还是要保持对数据结构特性的基本判断需要随机访问时用vector需要稳定顺序访问且很少删除时用deque需要频繁插入删除且稳定迭代器时再认真测试hive。建议把这篇文章里的基准代码打包成一个独立小工程持续跑在自己 CI 或本机脚本里。以后编译器升级、标准库版本更新时重新跑一遍能快速判断自己项目里的容器选型是否需要调整。
返回列表