后台开发很常见一大类需求是 线程安全 高性能 容器数据结构 开源的 https://github.com/greg7mdp/parallel-hashmap parallel-hashmap 是对 Google 的 abseil-cpp 库的改进,可供开发中直接使用。
参考官网的英文文档,简单翻译介绍如下:
[TOC]
parallel-hashmap 提供了一组卓越的 hash map 实现, 和 可以替代 std::map 和 std::set 的 btree 实现,并有如下特点:
std::unordered_map
, std::unordered_set
, std::map
and std::set
。 原样替换,api 完全兼容标准 stl 容器 。
try_emplace
)
phmap_fwd_decl.h
来声明 Parallel Hashmap 容器就行 [注: 目前这种对包含指针作为 key 的哈希表不适用。]
flat
哈希表存储了 std::trivially_copyable
的数据时, 表可以被 dump 到磁盘文件,并作为一个简单的数组高效地 restore 恢复,并且过程中不需要进行 hash 计算。这通常比逐个元素地序列化到磁盘快 10倍,但是会额外占用 10% - 60% 的磁盘空间。 见 examples/serialize.cc
. (flat hash map/set only)
examples/hash_value.h
). 并且提供 std::pair
和 std::tuple
的哈希函数实现.
作者有个介绍文章 讲解了 Parallel Hashmap 的设计和好处.
总结起来说,Google absl 的 flat_hash_map 由于采用 2倍的内存增长 resize 策略,所以有个内存占用的尖峰 peak, 在尖峰时刻,需要3倍的临时内存占用,来把旧的 bucket 里面的数据移动到 新的2倍大的 bucket 里面。
文章提出的想法就是,把一个大的 flat_hash_map 拆分成2的倍数个比如 16个,这样内存峰值就只有原来的 1/16 了。
并且这种拆分还便于进行并发。
本库提供的 hashmap 和 btree 基于 Google 在 Abseil 库中开源的实现。 hashmap 使用了 closed hashing,就是 value 直接存成一个内存数组,避免内存间接访问。通过使用并发的 SSE2 指令,这些 hashmap 可以允许一次并发查找 16 个槽位,即使当 hashmap 的填充率已经达到 87.5% ,也可以保持快速。
重要: 本仓库借鉴了 abseil-cpp 仓库的代码, 做了修改,并且可能和原版本行为不同。本仓库是独立工作,像其他开源项目一样不提供保证。请访问 abseil-cpp 获取官方的 Abseil 库。
无需安装,代码里面直接 include 就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #include <iostream> #include <string> #include <parallel_hashmap/phmap.h> using phmap::flat_hash_map; int main() { // Create an unordered_map of three strings (that map to strings) flat_hash_map<std::string, std::string> email = { { "tom", "tom@gmail.com"}, { "jeff", "jk@gmail.com"}, { "jim", "jimg@microsoft.com"} }; // Iterate and print keys and values for (const auto& n : email) std::cout << n.first << "'s email is: " << n.second << "\n"; // Add a new entry email["bill"] = "bg@whatever.com"; // and print it std::cout << "bill's email is: " << email["bill"] << "\n"; return 0; } |
---|
头文件 parallel_hashmap/phmap.h
提供下列 8 种 哈希表 :
头文件 parallel_hashmap/btree.h
提供一下基于 btree 的有序容器 ordered containers:
btree 容器是直接移植了 Abseil,因此应该是 Abseil 的表现一样,除了细微不同(例如支持 std::string_view 而不是 absl::string_view,并且有前向声明)
当 btree 被修改,value 可能在内存中被移动。这意味着当 btree 容器被修改时,指向 btree 容器中的 value 的指针或者迭代器会失效。 这和 std::map
/ std::set
显著不同, std 容器提供指针稳定性的保证。 ‘flat’ hash map 和 set 也会有这种失效。
全部的类型和模板参数可以在这个头文件看到: parallel_hashmap/phmap_fwd_decl.h , 这个头文件也可以用于前向声明 Parallel Hashmap 库。
哈希容器的关键设计点
flat
的哈希表内部会移动 key 和 value,所以如果在外部包含了指向 flat
哈希表中元素的指针,当哈希表被修改时,指针可能会变成野指针。而名字包含 node
的哈希表内部不会移动 key 和 value,可以用在这种场合。
flat
系列哈希表内存占用更少,并且通常比 node
系列哈希表更快,所以尽可能使用 flat
系列。当然,例外情况是 value 很大(比如大于 100字节),在内存中移动时很慢的话,此时应使用 node
系列。
parallel
系列的哈希表。当你需要在少数个哈希表中存储非常大量的 value 时, 应该优先用 parallel
系列 哈希表。而如果你需要在大量 哈希表中,每个存储相对少量的元素时,优先用非 parallel
系列的哈希表。
parallel
系列哈希表的好处是:
a. 减少了 resize 时候内存占用的峰值。
b. 可以打开多线程支持 (由于内部拆分成多个子哈希表,所以可以借此实现并发安全。)
btree 系列容器中的关键设计
Btree 系列容器是有序容器,可以作为 std::map
和 std::set
的替代。他们在每个树节点中存储多个value,因此更缓存友好,并且内存占用显著更低。
通常都应该优先用 Btree 容器,来代替 STL 中默认的红黑树容器。也有例外:
当不需要顺序的时候, 通常哈希表容器是比 btree容器 更好的选择。
PHMAP_USE_ABSL_HASH
改变。
erase(iterator)
和 erase(const_iterator)
都返回指向被删除的元素的下一个元素的迭代器,和 std::unordered_map 一样. 并提供了一个非标准的 void _erase(iterator)
,用于不需要返回 value 的场合。
absl::string_view
这种新类型。 std::hash<>
支持的所有类型 phmap 都支持 (如果编译器提供了 std::string_view
那 phmap 当然也支持).
phmap.h
之前 定义宏 #define PHMAP_NON_DETERMINISTIC 1
来打开这种随机初始化 (参考 raw_hash_set_test.cc).
type | memory usage | additional peak memory usage when resizing |
---|---|---|
flat tables | ||
node tables | ||
parallel flat tables | ||
parallel node tables |
size() / bucket_count()
. 从 0.4375 (刚 resize 之后) 到 0.875 (在 resize 之前) 之间波动. bucket 数组的大小每次 resize 都 double。sizeof(void *) + 1
, 由于对 bucket 数组 中的每一个 entry, node 哈希表存储一个指针加一个字节的元数据,sizeof(C::value_type) + 1
.0.03
即 0.5 / 16
规则和 std::unordered_map
相同。
Operations | Invalidated |
---|---|
All read only operations, swap, std::swap | Never |
clear, rehash, reserve, operator= | Always |
insert, emplace, emplace_hint, operator[] | Only if rehash triggered |
erase | Only to the element erased |
不同于 std::map
和 std::set
, 任何修改操作都可能使得存活的迭代器失效。
Operations | Invalidated |
---|---|
All read only operations, swap, std::swap | Never |
clear, operator= | Always |
insert, emplace, emplace_hint, operator[] | Yes |
erase | Yes |
为了使用 flat_hash_set 或者 flat_hash_map,自定义类需要提供一个 哈希函数。可以通过以下2种方法之一实现:
hash_value()
friend 函数.
例子自己看官方文档吧, 就不贴了。
Parallel Hashmap 容器遵循 C++ 标准库的线程安全规则。具体地:
if_contains()
安全地进行, 通过持有 submap 的锁,并把 value 的引用传给回调函数。类似地, 用 modify_if
或 try_emplace_l
可以进行安全的写操作。但是要注意,标准 API 返回的迭代器或者引用并没有被 mutex 保护,所以当另一个线程可能修改哈希表时,不能可靠地使用他们。
examples/bench.cc
(推荐使用 C++17 的 std::shared_mutex ,因为实测性能最好)
parallel_系列容器的线程安全的函数有:
多线程的例子,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | #include <assert.h> #include <stdint.h> #include <map> #include <shared_mutex> #include <thread> #include <unordered_map> #include <vector> #include "mm3rd/parallel-hashmap/parallel_hashmap/phmap.h" // 替代:std::unordered_map<uint64_t, uint32_t> typedef phmap::parallel_flat_hash_map<uint64_t, uint32_t, phmap::priv::hash_default_hash<uint64_t>, phmap::priv::hash_default_eq<uint64_t>, std::allocator<std::pair<const uint64_t, uint32_t>>, 4, std::shared_mutex> MapT; int main() { MapT index; const static uint32_t key_num = 10000; const static int thread_num = 10; for (uint64_t key = 0; key < key_num; ++key) { index.try_emplace_l( key, [](uint32_t& val) { val = 0; }, 0); } std::vector<std::thread> thread_list; for (int i = 0; i < thread_num; ++i) { thread_list.push_back(std::thread([&]() { for (uint64_t key = 0; key < key_num * 10; ++key) { index.modify_if(key, [](uint32_t& val) { ++val; }); } })); } for (auto& t : thread_list) { t.join(); } uint32_t hit_num = 0; for (uint64_t key = 0; key < key_num * 10; ++key) { index.if_contains(key, [&](const uint32_t& val) { assert(thread_num == val); ++hit_num; }); } assert(hit_num == key_num); return 0; } |
---|
我的博客即将同步至腾讯云+社区,邀请大家一同入驻:(https://cloud.tencent.com/developer/support-plan?invite_code=3qw4g1gk0xgkc)