phmap —— 缓存友好的高效hashtable

众所周知,C++标准库的 unordered_map在性能上向来不是一个好的选择。开源市场上有非常多的高性能哈希表可供选择,phmap继承自 absl-hashmap,有着非常好的插入、查找性能。在著名的Comprehensive C++ Hashmap Benchmarks 2022榜单中名列前茅。事实上,我比对了 phmap::flat_hash_map与榜单中综合性能第一的 ankerl::unordered_dense::map,我的benchmark中只有遍历哈希表时,flat_hash_map的性能低于 unordered_dense::map,其余无论是插入还是随即查找,大部分情况下 flat_hash_map的性能都更优。本文简单介绍了 flat_hash_map相关情况,以及一些使用上的建议与坑点。 flat_hash_map 和 node_hash_map区别 phmap中有提供了两类哈希表,其内部布局示意图如下: 由上图(忽略了bucket的细节)可以看出,flat_hash_map的最大的优点在于 node之间的内存是连续的(虽然可能中间存在空node),遍历的时候对cache更加友好 并且相比于 node_hash_map版少一次寻址过程(std::unordered_map的设计与 node_hash_map)相同。 而 flat_*系列的缺点就是在 rehash的时候: 会引发原来的value失效(这里的失效指的是原来的那个对象所对应的内存失效,而不是value所包含的内容失效,例如,value是一个指针,那它的值——所指向的对象,不会受到影响)。举个例子: flat_hash_map<int, Data> mp; node_hash_map<int, Data> nodemp; mp[0] = Data(); nodemp[0] = Data(); auto& mp0 = mp[0]; auto& nodemp0 = nodemp[0]; // tigger rehash for (int i = 1; i <= 10; i ++) { mp[i] = Data(); nodemp[i] = Data(); } assert(std::addressof(mp[0]) != std::addressof(mp0)); assert(std::addressof(nodemp[0]) == std::addressof(nodemp0)); 原因就是 flat_hash_map的内存布局导致的。而 node_hash_map或者 std::unordered_map就保证不会出现这种情况,因为当他们rehash的时候,只需要将bucket内的指针重新分配,指针的值还是指向原来的 node<key, value>....

July 23, 2023 · 3 min

将Wavelet Tree用于算法竞赛

Wavelet Tree for Competitive Programming 最近在学FM-Index相关算法用于数据库,了解到Wavelet Tree这一数据结构,发现其还可以应用在算法竞赛中。网上相关中文资料比较少,权当自己做个学习笔记 开始之前 在学习wavelet tree前,不妨看看他能解决什么样的问题。 假设我们有一长为 $n$ 的序列 $A[0…n - 1]$ 。在算法竞赛中,典型的数据量是 $n = 1e5, |A[i]| <= 1e9$ 区间 $[L, R)$ 中元素$x$的出现次数 区间 $[L, R)$ 中的第k小数 区间 $[L, R)$ 上 小于等于x的数的个数 … 以上问题都可以通过可持久化线段树在解决。那为什么还需要wavelet tree呢,我们都知道可持久化线段树的常数很大,并且十分消耗空间,在有些苛刻的题目下可能会被卡 好吧应该都是金牌题,不是我该考虑的 。利用wavelet tree可以在$log(\sigma)$时间内完成的同时(且优秀的常数),若使用bitvector优化空间,空间上大概比可持久化线段树少一个量级。最重要的一点是,我个人觉得他比主席树更加直观易懂。 $\sigma$ = | $\Sigma = {1, 2, \cdots, \sigma}$| (用于序列上时是值域大小)。 用wavelet tree的缺点就是带修改操作比较难写,码量较大,一般不会在比赛时使用。 Wavelet Tree 该图给出了用序列 $A = [7, 3, 5, 6, 1, 3, 2, 7, 8, 4]$ 构建的wavelet tree的形态。对于树上的每个节点,我们会将其按照值域分成两个部分$[low, mid), [mid, high)$。通过 稳定划分(stable_partition,即不改变相对顺序的情况下划分)将该节点上的序列中小于 $mid$的划分到左子树中,大于等于mid的划分到右子树中,递归直至节点中只有一种值时为叶节点。需要注意的是,我们并不会在叶子节点中直接存储序列的值,而是通过某个方法使得我们能够使用较小的空间的情况下得到足够的信息。...

January 10, 2023 · 4 min