Skip to content

flat_map 设计指南(三):测试策略与性能对比

上一篇撸完实现,笔者心里其实没那么踏实——flat_tree 那一坨 lower_bound + emplace + shift 能编过是一回事,语义对不对是另一回事。容器这种东西最怕“看起来能跑”:您塞几个数进去遍历出来有序,绿了;可重复 key 去重、insert_or_assign 的覆写、sorted_unique 撒谎、迭代器失效这些坑,全压在边界上。这一篇咱们就把第一篇承诺的六条不变量一条条摁回测试里,再拿真实测量跟 std::mapabsl::btree_map 比一比,看 flat_map 到底省在哪、又把代价付在了哪。套路跟 WeakPtr 设计指南(三) 一脉相承:不变量驱动,数据说话,不空口。

不变量 → 测试矩阵

#不变量断言
1有序遍历得到严格升序 key
2唯一重复 key 被去重
3查找语义find/contains/operator[]/at 对;at 越界 CHECK/assert
4insert_or_assign/try_emplace已存在一个覆写一个不动
5sorted_unique跳过排序;撒谎 debug abort
6迭代器失效mutation 后旧迭代器失效(粗规则)

关键用例(Catch2 风格示意)

六条不变量听着抽象,落到测试上其实就挑那些“写错了一定会爆”的边界。笔者这里挑三条最能锁语义的——构造期有序去重、insert_or_assign 的覆写、sorted_unique 撒谎 debug abort。配套工程在 code/volumn_codes/vol9/full_tutorial_codes/chrome_design/19~22 那几个 demo .cpp,Catch2 测试目标接入留作扩展,这里先看用例长什么样:

cpp
TEST_CASE("flat_map sorts+uniques on construction", "[flat_map]") {
    flat_map<int,int> m{{3,30},{1,10},{3,30},{2,20}};
    std::vector<int> keys;
    for (auto& [k,v] : m) keys.push_back(k);
    REQUIRE(keys == std::vector<int>{1,2,3});   // 不变量 1+2
}

TEST_CASE("insert_or_assign overwrites existing", "[flat_map]") {
    flat_map<int,int> m{{1,10}};
    auto [it, ins] = m.insert_or_assign(1, 99);
    REQUIRE_FALSE(ins); REQUIRE(it->second == 99);   // 不变量 4
}

TEST_CASE("sorted_unique aborts on lying input", "[flat_map][.death]") {
    // 不变量 5:传未排序数据却宣誓 sorted_unique → debug abort
    // flat_map<int,int> m(sorted_unique, std::vector<std::pair<int,int>>{{3,3},{1,1}});
}

这三条盯的都是语义边界,不是 API 表面。构造期那条把不变量 1 和 2 一起验——丢进去的 {3,30} 重复了一份、顺序也是乱的,遍历出来必须正好是 1,2,3,差一个就是 sort_and_unique 写错了;insert_or_assign 那条更细,笔者特意拿 ins 的 false 跟 it->second==99 对照,就是怕把“插入了新的”和“覆写了旧的”混成一回事。sorted_unique 撒谎那条单独拎出来,因为它会 abort。

会 abort 的用例有个麻烦:直接塞进普通 TEST_CASE 里跑,整个二进制都得跟着挂。得隔离成 death test,让它在子进程里崩——这套路数跟 01-6 处理 OnceCallback 单次消费断言、跟 WeakPtr 的 CHECK-on-deref 是同一套,笔者在那两篇已经趟过一遍了。

性能:per-item 开销

text
sizeof(flat_map<int,int>)  ≈ 24 字节(三指针)
sizeof(std::map<int,int>)  ≈ 48 字节(树根 + sentinel + 比较器)

100 万元素 map<int,int> 额外开销(数据 8MB 不计):
  flat_map:  ~0 额外(数据连续)
  std::map:  ~32MB(32B/元素 × 100 万 + 100 万次 malloc)

flat_map 零 per-item 元数据,一次连续分配就齐活;std::map 每元素背着 32B 元数据,还得一次堆分配。这正是第一篇说的“常数因子差一个数量级”的出处——渐近都是 O(log n) 查找,但 std::map 在咱们看不到的地方先把 32MB 元数据 + 100 万次 malloc 的账给记上了。

性能:查找 vs 插入

光看 sizeof 和分配次数还不过瘾,笔者上机实跑了一把。本机 GCC 16 -O2,配套 20_lookup_vs_shift_perf,10 万元素 map<int,int>:

text
查找 10 万次(10 万元素):
  flat_map:  31 ms
  std::map:  34 ms     (int key + 10 万:几乎持平)

插入 1000 次到 10 万元素容器:
  flat_map:   2 ms     (每次 O(n) shift)
  std::map:   0 ms     (O(log n) 节点重连)

这里有个数字笔者第一次看愣了一下——查找上 flat_map 居然没把 std::map 按在地上摩擦,俩几乎持平。后来一琢磨就通了:10 万 + int key 这个量级,数据本身就塞得进 cache,std::map 的指针追逐还没开始大规模 miss,flat_map 的连续红利自然显不出来。真想看出差距,得把 N 再往上推,或者把 key 换成 std::string 这种重的。独立大 N 测试里 flat_map 快出几倍是常有的事,但您别拿这当教条,小 N 轻 key 下它就是优势不明显,这很正常。

插入这边画风就反过来了,而且没什么悬念。flat_map 每插一个就得 O(n) shift 一大片,2 ms 对 std::map 的 0 ms;N 越大这个差距只会越拉越开,是面实打实的墙。所以 flat_map 的契约写得很明白:查多写少的主场,别拿它当高频写的容器使。

选型判据

负载推荐理由
写一次读多次(配置表/命令分发)flat_map写入一次性,查找 cache 友好
始终很小(~4 元素)flat_map常数因子主导,零分配
大且频繁改std::mapO(n) 插入是墙
需引用/指针稳定std::mapflat_map 迭代器全失效
大 N + 频繁改 + 有序absl::btree_mapB-tree 中间解(Chromium 因代码膨胀禁用)

这张表其实就是一句话的事:查多写少、或者容器一直很小,选 flat_map;写多、或者要引用指针稳定,选 std::map;又大又频繁改还非要有序,那就是 absl::btree_map 的中间解,但 Chromium 自己因为代码膨胀把这条路堵了。Chromium //base/containers/README.md 的划法,也就是把这张表落成了文字。

vs std::flat_map(C++23)/ absl::btree_map

聊到这儿,绕不开两个亲戚。C++23 的 std::flat_map(P0429)跟 Chromium flat_map 同源,思路是一脉的,但标准版选了 split storage——键和值分两个数组存。Chromium 偏不 split,老老实实用单个 vector<pair<K,V>>。笔者理解 Chromium 的取舍:split 在“只遍历 key”或“只遍历 value”时确实更省 cache,但实现复杂度上去了,而 flat_map 的主战场是查多写少的小容器,split 的收益在这儿根本兑现不出来,不划算。

另一个是 absl::btree_map,256B 的 B-tree 节点里塞一把 key,介于红黑树和 sorted vector 之间,大 N + 频繁改 + 有序的场景它最合适。但 Chromium 在 //base 里把 btree 禁用了,理由是代码膨胀——每实例化一种 key/value 类型都得多生成一大坨模板,B-tree 节点的拆装逻辑比 sorted vector 那套 lower_bound + shift 重得多。这是工程权衡的典型样本:技术上有更优解,但项目级别的代价吃不消,就宁可不要。

到这儿,flat_map 组件的设计、实现、验证三篇就走完了。回头看,它跟 OnceCallback、WeakPtr 凑成了 vol9/chrome 的第三块拼图——前两块讲的是“怎么把回调管住”“怎么把生命周期管住”,这一块讲的是“怎么把数据存得既省又快”,都是 Chromium //base 工业级 C++ 的基本功。

参考资源

v0.10.0-6-gbcee94e · bcee94e · 2026-08-20