shines77 / hashmap-benchmark

A hash map benchmark, include C++, Java, CSharp, golang and so on.
Other
7 stars 0 forks source link

加入我的hash_map #1

Open ktprime opened 2 years ago

ktprime commented 2 years ago

https://github.com/ktprime/emhash 其中 emhash7 整数类型k.v速度比较快,负载因子load_factor 可以设置接近1.0 emhash8 是dense_hash_map 实现,迭代,非整数类型k.v 以及查找命中不错性能 , 删除比较慢

shines77 commented 2 years ago

https://gitee.com/shines77/emhash 已经审核通过了。

https://github.com/shines77/jstd_hashmap 也修改了一个错误,开启了 hash_policy 。

可以使用下面命令更新:

git pull
git submodule update --init --recursive

如果你已经手动添加过 ./3rd_party/emhash,可以参考这个文章删除子模块:

https://github.com/shines77/my_docs/blob/master/linux/git/git-submodule-%E7%AE%80%E6%98%8E%E6%89%8B%E5%86%8C.md

然后使用下面命令添加 submodule :

git submodule add --force -b master -- "https://gitee.com/shines77/emhash.git" "./3rd_party/emhash"

该命令最好在前面两步完成了再使用,之后应该就能正常使用 git submodule update --init --recursive 更新 ./3rd_party/emhash 了。

shines77 commented 2 years ago

我的hashmap 里面有一个性能分析函数dump_statics()(宏开启),统计查找命中,失败,冲突率,cache miss 之类非常全面性能数据

我也写过类似的:

https://github.com/shines77/jstd_hashmap/blob/master/src/jstd/hashmap/hashmap_analyzer.h

不过,对于非链表的哈希表,要改一下。

ktprime commented 2 years ago

/mnt/f/emhash/bench/hashmap-benchmark/3rd_party/jstd_hashmap/src/jstd/hashmap/robin_hash_map.h:2596:53: error: no matching function for call to ‘jstd::robin_hash_map<HashObject<long unsigned int, 256, 32>, long unsigned int, HashFn<long unsigned int, 256, 32, false>, std::equal_to<HashObject<long unsigned int, 256, 32>>, std::allocator<std::pair<HashObject<long unsigned int, 256, 32>* const, long unsigned int> > >::map_group::matchHashAndDistance(uint8_t&, uint8_t&) const’ 2596 | auto mask32 = group.matchHashAndDistance(ctrl_hash, distance);

image

shines77 commented 2 years ago

/jstd/hashmap/robin_hash_map.h:2596 这行的错误,改成:

group.matchHashAndDistance(dist_and_hash);

不过,这个文件的版本有点旧了吧,最新的代码不是这样的。那些 warning 可以用编译开关关掉,只是拿来调试用的。

另外,最好不要在你的 emhash 里以 submodule 的方式引入 /hashmap-benchmark,因为可能会造成循环引用。可以 submodule 我的 /jstd_hashmap,因为后者没有引入其他库。此外,/jstd_hashmap 的代码其实是 "Header only" 的,CMakeLists.txt 做了一个 jstd 的静态库只是为以后准备的,不是必须的。因为使用了 SIMD 指令,编译选项里保证有 -march=native ,让编译器能检测是否支持 SIMD 就可以了,最低要求 C++11。

ktprime commented 2 years ago
 template<class hash_type>
 void find_hit_100(const hash_type& ht_hash, const std::string& hash_name, const std::vector<keyType>& vList)
 {
     auto ts1 = getus(); size_t sum = 0;
     for (const auto& v : vList) {
         sum += ht_hash.count(v);
 #if FL1
         if (sum % (1024 * 64) == 0)
             memset(l1_cache, 0, sizeof(l1_cache)); //清楚L1缓存
 #endif
     }
     check_func_result(hash_name, __FUNCTION__, sum, ts1);
 }

一直在想如何做出一个比较客或符合实际的性能测试, 比如我ebench测试程序100%命中查找, 随机模拟偶尔刷掉L1/2级缓存, vList 输入数据是哈希ht_hash部分key数据(10%数据子集),有重复(不需要命中hash 中所有key)

ktprime commented 2 years ago

emhash7 默认负载0.98 加入测试,效果还行

run_insert_random: jstd::flat16_hash_map<size_t, size_t> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 483.16 ms hashmap.find(key), check_sum: 81835627463776, average: 21.88 ns , time: 358.55 ms

run_insert_random: jstd::robin16_hash_map<size_t, size_t> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 699.13 ms hashmap.find(key), check_sum: 81835627463776, average: 21.84 ns , time: 357.79 ms

run_insert_random: ska::flat_hash_map<size_t, size_t> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 641.22 ms hashmap.find(key), check_sum: 81835627463776, average: 15.80 ns , time: 258.93 ms

run_insert_random: absl::flat_hash_map<size_t, size_t> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 503.19 ms hashmap.find(key), check_sum: 81835627463776, average: 19.41 ns , time: 317.99 ms

run_insert_random: emhash5::HashMap<size_t, size_t> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 756.88 ms hashmap.find(key), check_sum: 81835627463776, average: 13.40 ns , time: 219.49 ms

run_insert_random: emhash7::HashMap<size_t, size_t> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.937, time: 609.78 ms hashmap.find(key), check_sum: 81835627463776, average: 17.64 ns , time: 288.96 ms


run_insert_random: jstd::flat16_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 529.10 ms hashmap.find(key), check_sum: 133458100370101, average: 23.84 ns , time: 390.51 ms

run_insert_random: jstd::robin16_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 806.62 ms hashmap.find(key), check_sum: 133458100370101, average: 22.50 ns , time: 368.65 ms

run_insert_random: ska::flat_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1023.87 ms hashmap.find(key), check_sum: 133458100370101, average: 17.11 ns , time: 280.26 ms

run_insert_random: absl::flat_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 725.70 ms hashmap.find(key), check_sum: 133458100370101, average: 21.15 ns , time: 346.57 ms

run_insert_random: emhash5::HashMap<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1151.76 ms hashmap.find(key), check_sum: 133458100370101, average: 13.65 ns , time: 223.61 ms

run_insert_random: emhash7::HashMap<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.968, time: 800.35 ms hashmap.find(key), check_sum: 133458100370101, average: 18.97 ns , time: 310.86 ms


ktprime commented 2 years ago

value 改成int了

run_insert_random: jstd::flat16_hash_map<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 554.17 ms hashmap.find(key), check_sum: 133458100370101, average: 22.90 ns , time: 375.14 ms

run_insert_random: jstd::robin16_hash_map<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 825.39 ms hashmap.find(key), check_sum: 133458100370101, average: 22.82 ns , time: 373.84 ms

run_insert_random: ska::flat_hash_map<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1007.99 ms hashmap.find(key), check_sum: 133458100370101, average: 17.11 ns , time: 280.40 ms

run_insert_random: absl::flat_hash_map<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 798.31 ms hashmap.find(key), check_sum: 133458100370101, average: 21.25 ns , time: 348.15 ms

run_insert_random: emhash5::HashMap<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 975.27 ms hashmap.find(key), check_sum: 133458100370101, average: 11.88 ns , time: 194.72 ms

run_insert_random: emhash7::HashMap<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.968, time: 759.62 ms hashmap.find(key), check_sum: 133458100370101, average: 18.04 ns , time: 295.54 ms


shines77 commented 2 years ago

为什么在你的测试里,jstd::flat16_hash_map 这么快?是因为用 clang 编译的原因吗?在我两台云服务器里,jstd::flat16_hash_map 都是最慢的那一个。

ktprime commented 2 years ago

为什么在你的测试里,jstd::flat16_hash_map 这么快?是因为用 clang 编译的原因吗?在我两台云服务器里,jstd::flat16_hash_map 都是最慢的那一个。

intel 9700k GCC 11.1 + wsl2 ubu2004

下面是 clang12 结果差不多 (云服务cpu 单核性能太低,但缓存大一些)

run_insert_random: jstd::flat16_hash_map<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 642.42 ms hashmap.find(key), check_sum: 133458100370101, average: 22.55 ns , time: 369.53 ms

run_insert_random: jstd::robin16_hash_map<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 929.18 ms hashmap.find(key), check_sum: 133458100370101, average: 28.73 ns , time: 470.77 ms

run_insert_random: ska::flat_hash_map<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 963.37 ms hashmap.find(key), check_sum: 133458100370101, average: 16.29 ns , time: 266.95 ms

run_insert_random: absl::flat_hash_map<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 713.61 ms hashmap.find(key), check_sum: 133458100370101, average: 21.24 ns , time: 347.92 ms

run_insert_random: emhash5::HashMap<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1053.21 ms hashmap.find(key), check_sum: 133458100370101, average: 12.68 ns , time: 207.76 ms

run_insert_random: emhash7::HashMap<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.968, time: 840.93 ms hashmap.find(key), check_sum: 133458100370101, average: 18.64 ns , time: 305.33 ms


ktprime commented 2 years ago

//linux server Intel(R) Xeon(R) Silver 4210 CPU @ 2.20GHz gcc 7.5 ubu1604

run_insert_random: jstd::flat16_hash_map<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1530.20 ms hashmap.find(key), check_sum: 133458100370101, average: 45.34 ns , time: 742.88 ms

run_insert_random: jstd::robin16_hash_map<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1847.26 ms hashmap.find(key), check_sum: 133458100370101, average: 38.43 ns , time: 629.56 ms

run_insert_random: ska::flat_hash_map<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 2146.28 ms hashmap.find(key), check_sum: 133458100370101, average: 33.75 ns , time: 552.90 ms

run_insert_random: absl::flat_hash_map<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1456.10 ms hashmap.find(key), check_sum: 133458100370101, average: 39.79 ns , time: 651.93 ms

run_insert_random: emhash5::HashMap<size_t, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 2007.20 ms hashmap.find(key), check_sum: 133458100370101, average: 24.60 ns , time: 403.11 ms

shines77 commented 2 years ago

我也换成 GCC 11.1 了,刚开始看你用的 GCC 6.x,我也换成 GCC 6.x 测试一些可能版本不兼容的问题。

AMD EPYC 7K62 48核(2.6GHz,2核4G内存,Ubuntu 18.04,GCC 11.1):

DataSize = 16384000, test::MumHash


run_insert_random: jstd::flat16_hash_map<int, int> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 1680.51 ms hashmap.find(key), check_sum: 81835627463776, average: 50.34 ns, time: 824.71 ms

run_insert_random: jstd::robin16_hash_map<int, int> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 1089.93 ms hashmap.find(key), check_sum: 81835627463776, average: 30.84 ns, time: 505.28 ms

run_insert_random: jstd::robin_hash_map<int, int> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 1134.90 ms hashmap.find(key), check_sum: 81835627463776, average: 32.33 ns, time: 529.61 ms

run_insert_random: jstd::flat16_hash_map<int, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 2660.54 ms hashmap.find(key), check_sum: 133458100370101, average: 51.31 ns, time: 840.69 ms

run_insert_random: jstd::robin16_hash_map<int, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1279.80 ms hashmap.find(key), check_sum: 133458100370101, average: 34.15 ns, time: 559.44 ms

run_insert_random: jstd::robin_hash_map<int, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1333.04 ms hashmap.find(key), check_sum: 133458100370101, average: 34.12 ns, time: 558.97 ms


run_insert_random: jstd::flat16_hash_map<size_t, size_t> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 1808.19 ms hashmap.find(key), check_sum: 81835627463776, average: 50.73 ns, time: 831.09 ms

run_insert_random: jstd::robin16_hash_map<size_t, size_t> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 1177.34 ms hashmap.find(key), check_sum: 81835627463776, average: 32.06 ns, time: 525.34 ms

run_insert_random: jstd::robin_hash_map<size_t, size_t> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 1240.16 ms hashmap.find(key), check_sum: 81835627463776, average: 33.09 ns, time: 542.10 ms

run_insert_random: jstd::flat16_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 2863.93 ms hashmap.find(key), check_sum: 133458100370101, average: 51.69 ns, time: 846.85 ms

run_insert_random: jstd::robin16_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1451.17 ms hashmap.find(key), check_sum: 133458100370101, average: 34.37 ns, time: 563.17 ms

run_insert_random: jstd::robin_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1563.32 ms hashmap.find(key), check_sum: 133458100370101, average: 36.03 ns, time: 590.38 ms

ktprime commented 2 years ago

编译器强烈推荐最新版本低1个大版本的,vs/clang/g++ 我一直使用最新的(几乎每天都要查看是否有新版本可更新:) gcc12+clang14+vs2022 )。主要是优化性能更好

云主机有可能和别人共享cpu ,测试有可能不准.

cmake 里面可以添加 flto 之类的优化 set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)

shines77 commented 2 years ago

云服务器还好,CPU基本是够用的,没那么忙,我看主要还是 CPU 架构的问题,但是我实在想不出来为什么会差这么多。。

这台 Intel 的云服务器三级缓存更小一点,性能更差。我懒得重新编译 hashmap_benchmark 了,ska::flat_hash_map,emhash5 与 jstd::robin_hash_map 的结果比较接近。

Intel Xeon Platinum 8255C 24核(2.5GHz,两核4GB,Ubuntu 20.04,GCC 11.1):

DataSize = 16384000, test::MumHash


run_insert_random: jstd::flat16_hash_map<int, int> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 2354.04 ms hashmap.find(key), check_sum: 81835627463776, average: 38.94 ns, time: 638.02 ms

run_insert_random: jstd::robin16_hash_map<int, int> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 1214.23 ms hashmap.find(key), check_sum: 81835627463776, average: 38.55 ns, time: 631.53 ms

run_insert_random: jstd::robin_hash_map<int, int> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 1237.25 ms hashmap.find(key), check_sum: 81835627463776, average: 43.52 ns, time: 712.99 ms

run_insert_random: jstd::flat16_hash_map<int, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 5699.52 ms hashmap.find(key), check_sum: 133458100370101, average: 42.21 ns, time: 691.54 ms

run_insert_random: jstd::robin16_hash_map<int, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1416.67 ms hashmap.find(key), check_sum: 133458100370101, average: 43.72 ns, time: 716.36 ms

run_insert_random: jstd::robin_hash_map<int, int> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1474.88 ms hashmap.find(key), check_sum: 133458100370101, average: 49.48 ns, time: 810.71 ms


run_insert_random: jstd::flat16_hash_map<size_t, size_t> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 2527.06 ms hashmap.find(key), check_sum: 81835627463776, average: 42.19 ns, time: 691.27 ms

run_insert_random: jstd::robin16_hash_map<size_t, size_t> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 1395.27 ms hashmap.find(key), check_sum: 81835627463776, average: 41.39 ns, time: 678.19 ms

run_insert_random: jstd::robin_hash_map<size_t, size_t> hashmap.size() = 7856563, cardinal = 9600000, load_factor = 0.468, time: 1429.24 ms hashmap.find(key), check_sum: 81835627463776, average: 44.20 ns, time: 724.16 ms

run_insert_random: jstd::flat16_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 6111.75 ms hashmap.find(key), check_sum: 133458100370101, average: 43.93 ns, time: 719.75 ms

run_insert_random: jstd::robin16_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1780.37 ms hashmap.find(key), check_sum: 133458100370101, average: 45.35 ns, time: 743.01 ms

run_insert_random: jstd::robin_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1818.44 ms hashmap.find(key), check_sum: 133458100370101, average: 48.73 ns, time: 798.40 ms

ktprime commented 2 years ago

我的云服务器 AMD EPYC 7K62 48-Core Processor clang12 ubu2004

run_insert_random: std::unordered_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.660, time: 7709.28 ms hashmap.find(key), check_sum: 133458100370101, average: 53.04 ns , time: 869.03 ms

run_insert_random: jstd::flat16_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 3140.10 ms hashmap.find(key), check_sum: 133458100370101, average: 46.55 ns , time: 762.68 ms

run_insert_random: jstd::robin16_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1796.43 ms hashmap.find(key), check_sum: 133458100370101, average: 45.76 ns , time: 749.77 ms

run_insert_random: jstd::robin_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1555.68 ms hashmap.find(key), check_sum: 133458100370101, average: 30.48 ns , time: 499.32 ms

run_insert_random: ska::flat_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1629.39 ms hashmap.find(key), check_sum: 133458100370101, average: 25.78 ns , time: 422.44 ms

run_insert_random: emhash::HashMap<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1934.08 ms hashmap.find(key), check_sum: 133458100370101, average: 22.62 ns , time: 370.69 ms

run_insert_random: absl::flat_hash_map<size_t, size_t> hashmap.size() = 16244874, cardinal = 960000000, load_factor = 0.484, time: 1447.03 ms hashmap.find(key), check_sum: 133458100370101, average: 43.06 ns , time: 705.52 ms

shines77 commented 2 years ago

嗯,和我结果差不多,CPU好像也一样,应该是也是腾讯的云服务器吧。

哦,还是有点区别的,clang 12 下,absl::flat_hash_map 结果还不错。

ktprime commented 2 years ago

是的,只有腾讯云有这种AMD CPU 800RMB 3年用于编译测试 要测试各种组合 cpu+编译器+编译选项 我测试很多case,absl::flat_hash_map 总体还是最优的 你可以跑跑我目录下几个测试。 你的上面测试分别是插入和读取hit时间?

插入测试加一个reserve case。find 加一个find miss

shines77 commented 2 years ago

在我的数独求解程序里,有这么尝试过,但我的经验是,编译器这个东西不好说:

https://github.com/shines77/gudoku https://github.com/shines77/gz_sudoku

总体上来说,clang 要强一点,这两个程序,第一个基本是 SIMD,第二个也有少量 SIMD。

我甚至还装了 Linux 下的 Intel C++ Compiler(现在叫 Intel oneAPI),有的编译器新版本的还不如旧一点的。clang 有些版本还有些莫名的 bug (比如匿名namespace下变慢),编译器也不是万能的,也不一定新的就一定快,要看代码吧,不过一般来说,clang > gcc > msvc,新版本可能快于旧版本。Intel C++ Compiler 是基于 clang 或 gcc 的,它已经完全没有任何优势了。

clang 对于 SIMD(Intel Intrinsics),是会调整 load,store 的指令顺序,还会更换更快的 SIMD 指令,但有时候可能会弄巧成拙。GCC 不会调整 load,store 的顺序,也不会改变和优化(Intel Intrinsics),但是 gcc 的编译选项是比 clang 更丰富,细节更多,可以说更稳健吧,clang 更激进一点。

shines77 commented 2 years ago

cardinal_bench 是测试随机插入,插入的元素可能会重复,因为插入的同时也算做了一次 find(可能 hit,也可能 miss),所以我就也测了一次一模一样的 find_hit(),对比一下,两者时间相减大致可以理解为单纯插入的时间(虽然是不等价的),同时也可以观察一下 find_hit() 的性能。

shines77 commented 2 years ago

find failed 就是 find miss 啊,表述不一样而已

ktprime commented 2 years ago

https://github.com/martinus/map_benchmark 综合测试被我改成一个简单测试 容易直观对比各种hashmap性能 算是一个比较好的测试,你可以把你的实现放进去。

https://github.com/ktprime/emhash/blob/master/bench/martinus_bench.cpp

lighthouse@VM-8-17-ubuntu:~/emhash/bench$ ./mbench 2356


clang 12.0.0 on llvm/gcc 4.2.1 __cplusplus = 202002 x86-64 OS = linux, cpu = AMD EPYC 7K62 48-Core Processor

./test [2-9mptseb0d2 rjqf] n ankerl martinus dense emhash5 emhash5 emhash6 emhash6 emhash7 emhash7 emhash8 emhash8 emilib2 emilib2 emilib3 emilib3 cmd agrs = 23567 test hash: ankerl martinus dense emhash8 emhash8 bench_IterateIntegers map = emhash8 total iterate/removing time = 0.55, 1.66|83332083325000

bench_IterateIntegers map = martinus dense total iterate/removing time = 0.56, 1.66|83332083325000

bench_randomFindString map = emhash8 0% 13 byte success time = 1.56 s 3094 loadf = 0.48 25% 13 byte success time = 1.91 s 12501887 loadf = 0.48 50% 13 byte success time = 2.02 s 25000807 loadf = 0.48 75% 13 byte success time = 2.02 s 37499823 loadf = 0.48 100% 13 byte success time = 1.93 s 49998809 loadf = 0.48 0% 100 byte success time = 2.46 s 1154 loadf = 0.76 25% 100 byte success time = 3.42 s 24997607 loadf = 0.76 50% 100 byte success time = 3.53 s 49995539 loadf = 0.76 75% 100 byte success time = 3.03 s 74992241 loadf = 0.76 100% 100 byte success time = 3.29 s 99989650 loadf = 0.76 total time = 9.45 + 15.74 = 25.19

bench_randomFindString map = martinus dense 0% 13 byte success time = 1.53 s 3094 loadf = 0.48 25% 13 byte success time = 1.92 s 12501887 loadf = 0.48 50% 13 byte success time = 2.10 s 25000807 loadf = 0.48 75% 13 byte success time = 2.29 s 37499823 loadf = 0.48 100% 13 byte success time = 2.32 s 49998809 loadf = 0.48 0% 100 byte success time = 2.91 s 1154 loadf = 0.76 25% 100 byte success time = 3.91 s 24997607 loadf = 0.76 50% 100 byte success time = 4.17 s 49995539 loadf = 0.76 75% 100 byte success time = 4.57 s 74992241 loadf = 0.76 100% 100 byte success time = 4.68 s 99989650 loadf = 0.76 total time = 10.16 + 20.25 = 30.41

bench_randomEraseString map = emhash8 000000000001ffff time = 3.73, loadf = 0.50 65541 00000000000fffff time = 5.34, loadf = 0.50 523526 00000000000fffff time = 4.24, loadf = 0.50 523526 00000000000fffff time = 5.39, loadf = 0.50 523526 000000000007ffff time = 5.33, loadf = 0.50 262059 total time = 24.09 s

bench_randomEraseString map = martinus dense 000000000001ffff time = 2.73, loadf = 0.50 65541 00000000000fffff time = 5.13, loadf = 0.50 523526 00000000000fffff time = 4.95, loadf = 0.50 523526 00000000000fffff time = 5.13, loadf = 0.50 523526 000000000007ffff time = 4.75, loadf = 0.50 262059 total time = 22.75 s ....

shines77 commented 2 years ago

已经可以在 clang 下编译了,并且 CMakeLists.txt中已添加 LPO/LTO 的支持,由于开启 LPO/LTOgcc 下无法编译成功,所以只在 clang 中开启。使用 gccclang,或者是否开启 LPO/LTO,的确跟我估计的一样,有的变快了,有点变慢了,很难确定,相对来说还是 gcc 更稳定一点。

由于 LPO/LTO 是从 CMake 3.9 才开始支持,所以 CMakeLists.txt 里有完整的版本判断和处理流程,以自适应不同的 CMake 版本,同时也支持手动添加 LPO/LTO 编译选项,具体可以参阅 CMakeLists.txt 中的源码。

clang 编译的方法:

cd ./clang
./cmake_config.sh
make

或者在./clang 下直接执行命令:

cmake -G "Unix Makefiles" -DCMAKE_C_COMPILER=/usr/bin/clang -DCMAKE_CXX_COMPILER=/usr/bin/clang++ -DABSL_BUILD_TESTING=OFF -DABSL_USE_GOOGLETEST_HEAD=OFF -DABSL_PROPAGATE_CXX_STD=ON -DCMAKE_PREFIX_PATH=../install ./

同时,编写了一个清除 cmake 编译缓存的脚本:cmake_clean.sh,用于更换编译器版本,或者编译选项时,清除 CMake 的缓存。

ktprime commented 2 years ago

我在大型程序中使用lto 有比较大的性能10-20%

生成可执行程序 cmakelist.txt 加如下, cmake 升级到版本高一点。我喜欢用最新的

set(CMAKE_CXX_FLAGS "-ffunction-sections -fdata-sections -flto")
if (CMAKE_COMPILER_IS_GNUCXX)
set(CMAKE_CXX_FLAGS "-ffunction-sections -fdata-sections -flto -fwhole-program")
endif() 
ktprime commented 2 years ago

关于测试数据随机性, 没必要固定随机种子,希望每次数据都不一样

int main(int argc, char * argv[])
{
    jstd::RandomGen   RandomGen(time(0));
    jstd::MtRandomGen mtRandomGen(time(0));
  ...
}
ktprime commented 2 years ago

目前看你的实现 大量小数据 sizeof(k+v) < 20 插入方面有些优势,你的hashmap使用上能否更简单,直接include 单个头文件不需要其他(一堆宏和其他文件),最近想把clickhouse hashmap加入测试,发现接口和std 差异太大 。。。

测了一个hashset

image

shines77 commented 2 years ago

你应该说的是插入大量数据的时候,但目前 jstd::robin_hash_map 插入 “大对象” 的时候是很慢的,我本来想放到以后再实现的,但是我在写一篇 "怎样写一个高性能的哈希表?" 的文章,觉得还是很要先实现一部分 ”大对象“ 的处理逻辑,原理类似跟 https://github.com/martinus/unordered_dense 类似。


一直想回复你的,但一开始是在忙于大修改 jstd_hashmap,半个月前基本告一段落,有时间回复了,又一直忘记。

我最近在忙于写这篇文章 怎样写一个高性能的哈希表?,进展很慢,我写了可能快一个星期了,我真正想写的内容连切入都还没开始切入,可能还是我的命题起得太大了。写出来,很多东西跟我当初设想的很不一样,甚至可以说之前做的所有工作基本算是白费的,虽然已经有一个很好的积累,有这个积累才能更好实现后面的设想,但是。。。

其实我早就知道,虽然测试哈希表 <int, int>, <uint64_t, uint64_t> 这种 KV 也有一定的现实意义,但其实我心里是很清楚,哈希表在现实应用中,更有实际价值的还是 <string, string>, <string, uint64_t> 这种 “中对象” KV。

为此,在很多天前,我已经在对 /bench/time_hash_map 进行了大改,命名为 time_hash_map_new,因为原来 time_hash_map 定义的用来测试的 HashObject<T, Size, HashSize> 对象设计得还是有点蠢的,我在给 jstd::robin_hash_map 添加 kNeedStoreHash 功能的时候意识到了这一点,因为它即跟现实的应用很不一样,不太具有现实意义,但它测的对象其实是 HashObject<int, 4, 4> 和 HashObject<uint64_t, 8, 8>,虽然测试的结果跟测试<int, int>, <uint64_t, uint64_t> 的结果是基本一致的,但是很不科学。

给 time_hash_map_new 换了个壳,改为测试 <int, int>, <uint64_t, uint64_t>,<std::string, std::string>, <std::string_view, std::string_view>之后就没管了,<std::string, std::string>的代码一直并未实现(因为实现起来要做很多修改)。今天早上,我一咬牙,决定先暂停写那篇文章,把 <std::string, std::string>的测试代码写出来了,跟 <int, int>, <uint64_t, uint64_t> 的测试还是很不一样的,<std::string_view, std::string_view> 我晚一点再实现,虽然需要做的改动并不大。

其实,我想尽快把 time_hash_map_new 移植到我的 hashmap_benchmark 里的,想看看别的和别的哈希表的比较数据(虽然我知道其实和原来 time_hash_map 里面测试大对象 HashObject<size_t, 256, 32> 的结果出入不会很大),但是早上有点困,我就睡了一下,刚起来。

shines77 commented 2 years ago

怎样写一个高性能的哈希表? 这篇文章你真的可以看一看,以后我都会围绕这篇文章来做事情,虽然很多东西还没开始写,我真正的目的是要介绍一下,absl::flat_hash_map(Swiss Table)的演变,原理,优点和缺点;robin hood哈希表的原理,优点和缺点;布谷鸟哈希表的原理,优点和缺点。


我的 jstd::robin_hash_map 距离上一次的版本,已经做了很大的改进,以及很多的改动。

https://github.com/martinus/unordered_dense 的原理和工作方式,我也理解了,很难评价它的方式是传统的 robin hood 哈希表是好还是坏,但是他这种方式,在 find (包括 find hit 和 find miss)的时候,有可能更快。他的原理是,除了对 distance 排序,同时也顺便对 ctrl_hash 按升序或降序排序,这样即使搜索落在同一个 bucket 的数据时,不一定要扫描到终止点,也能提前知道扫描完了,因为除了 distance 是有序的,ctrl_hash 也是有序的。但是这样做的缺点就是,插入数据的时候,一旦找到插入的点,之后的数据就必须全部往后移动,以保持 ctrl_hash 值的有序性,这会导致移动的元素要比传统的 robin hood 哈希表多,虽然移动的只是 entry 的索引值 index 。


我的 jstd::robin_hash_map 做的改进:

  1. 由于之前的版本是从旧的版本复制过来的,插入和删除的逻辑是很龊的,我全部都改为比较合理的方式了,也比较清晰。
  2. Google 的 absl::flat_hash_mapPolicyTraitsAllocator 那一套,我也移植过来了,这样更完整,但是它的 emplace(....) 中 extract Key 的那一套(absl::apply(F, ...), absl::DecomposePair 等),我没有用,以后有机会再写一个跟它一样处理方式的版本。它利用 std::tuple 和 absl::apply,在静态编译阶段有可能跳过一些不必要的转换获取 key 或跟 key 等价的类型的值,有时候能提高性能。
  3. 实现了 swap_slot() 和 exchange_slot() 语义,由于没有 C++17 的 if constexpr () 语法糖,这个部分写起来很费劲,修改和完善,前后可能花了我一个星期的时间。为了针对有些有 this.swap(other) 接口的对象,实现更快的 swap,我还写了一个 swap_traits,期望在类似 std::string 等 Key 中取得更好的性能。
  4. 实现了智能判断是否使用 ctrl_hash 的逻辑,根据 kNeedStoreHash 的值决定,现在对与 Pod 类型的 Key 和 Value,或者满足 kNeedStoreHash = false 条件的 Key,Value,metadata 结构中不会包含 ctrl_hash,以达到更大的缓存利用率,实际效果还是很明显的,即使我采用的是 metadata 和 slot 数据分开保存的方式。因为对与 <int, int>, <uint64_t, uint64_t>,比较一次 ctrl_hash,和比较一次 int 或 uint64_t 的代价差不了太多,而且就算 ctrl_hash 相等,对于 key 还是要再比较一次才能确定是否 Find Hit 命中,那不如就省掉 ctrl_hash 的比较,但是在 “中、大对象” 中,还是保存和比较一下 ctrl_hash 更好,因为这样能减少尽量少污染 slot 的本不需要访问的数据,对于 “小对象” 污染一下问题不大,只要搜索的长度不是太长,Find 最终命中的数据可能已经缓存中了,就是用一次 ctrl_hash 是否相等的判断和可能的分支,和污染不该访问的小对象 slot 数据之前的权衡。
  5. 再次改进了 hash_policy 的工作方式,跟 aka::flat_hash_map 相比,增加了一些一直类的排除,某些 Key 类型的哈希函数已经有很好的随机分布,就不再做二次哈希了。后来我看了你的源码,我的这个二次哈希跟你的 emhash5 其实最终效果是类似的(一样的),虽然实现方式不一样。hash_policy 这种方式更适合 robin hood 哈希表,开启了 hash_policy,robin hood 哈希表会更健壮,但是性能可能会有一点点下降。
  6. 还有其他小改进,可能记不清了。你可以在 jstd_hashmap 里,运行 cardinal_bench 和 time_hash_map,对比 jstd::robin_hash_map 和 jstd::v2::robin_hash_map,jstd::v3::robin_hash_map 之间的数据,上一个应用于 hashmap_benchmark 仓库的版本,是介于 v2 和 v3 版之间的版本。
shines77 commented 2 years ago

哦,我的 hashmap 是否可以写成一个文件的,理论上当然是可以,但是这样代码并不是很好看,理论上,只要是 header only 的,都可以浓缩到一个头文件里的。原则上一般不会这么做,但如果确实有需求,可以弄一下。目前,我的哈希表都是 header only 的啊,只需要引入 submodule 或者把整个 /jstd 目录复制过去就可以了,不需要配置,也不需要写 CMakeLists.txt。他只不过是一个文件和多个文件的区别。

不过,今天早上测试 /bench/time_hash_map_new 的 <std::string, std::string> 的时候,我发现 jstd::robin_hash_map 和 jstd::v3::robin_hash_map 可能是存在 bug 的,因为 emplace_and_replace 的时候,哈希表的 load_factoer() 好像比单纯 emplace() 时的大,说明 size() 比正常的时候大。


clickhouse hashmap 在我写 cardinal_bench 参考的那篇文章里是有看到,但是我也没准备去弄,那篇文章有提到,他自己整理和提取了一个版本,但是我在 github, gitee 上没看到他修改的版本,如果有直接可用的版本,我就会试一下的。clickhouse hashmap 很早之前我看过一下的,接口都是他们自己弄的,它只需要方便处理他们的 SQL 语句就行,所以没有遵循 C++ 标准库的接口。

哦,说到这里,我想起了我的 jstd::robin_hash_map 另一个改进,就是用 prefetch 指令在 rehash() 的时候,对旧的 slot 做了一个读的 prefetch,本来 prefetch 最大的价值就是对新的 slot 做写的 prefetch,但目前我也还没有写,因为工程量比 读 prefetch 的要大很多,而且并不一定有很大的效果。实践下来,对旧的 slot 做读的 prefetch,提升是很微小的。。。

absl::flat_hashmap 只对 ctrl 这个指针在 find() 开始的时候,做了一次 prefetch_T2,意义也基本不大(甚至有一点点变慢,不同CPU效果不一样),他写了更完整的 prefetch 代码,但没有使用,因为确实一般情况下,用不了。唯一能用的也只有 rehash 的时候,但 Google 没有在 rehash 中使用 prefetch。

我之所以会想起来这个,是因为你提到的 clickhouse hashmap,让我想起来了我写 cardinal_bench 参考的那篇文章里,有使用 prefetch 技术,在数据量很大的时候,最终达到和 clickhouse hashmap 差不多,或更快的速度。

ktprime commented 2 years ago

目前看 prefetch对hashmap 似乎没太大作用,即便rehash 过程线性遍历未必有多大收益。要配合循环展开特殊应用场景行 字符串hashmap提升性能有一定难度,相当部分消耗hash计算和内存分配(配合定制内存池和保留字符串, hash 可以提升性能)

shines77 commented 2 years ago

哦,另外,我的 hashmap_benchmark,原来采用的是 C++14,但是 CMakeLists.txt 里有的地方写的 C++11,导致最终还是变成了以 C++11 的方式编译的,这个 bug 后来我发现和修正了。再后来,我干脆改成了 C++17。改成 C++ 14以后,gcc 的测试结果有所提升,改为 C++17 相比 C++14 好像提升不大。

/clang/CMakeLists.txt 中我还尝试了改为 llvm/clang 的 C++ 库 libc++,感觉变化不大,我又改回了 g++ 的 libstdc++。两者肯定是有差别的,但我没仔细分辨两者的差别。

此外,我还把你的 emhash7 也加入了 cardinal_bench 和 time_hash_map 测试,但由于在 time_hash_map 在测试 16 字节以上的大对象是,可能有bug,很慢很慢,我就在 time_hash_map 关掉了 emhash7 的测试。

另外,我的 hashmap,一般所用到的宏都写在 hashmap 头文件里,不会依赖别的头文件中的宏。如果说 /basic/stddef.h,/config/config.h 这种做基本配置文件中的宏也算的话,那还是有,但是它是无害的,没有这些配置文件应该也不影响使用,一般可能有 likely(), unlikely() 这些宏,至于 config.h,这个一般的库都是需要的,而且我的 hashmap 也不会直接依赖 config.h,但是 /basic/stddef.h 可能是有依赖的,刚才想了一下,除了 likely(),还有 JSTD_FORECE_INLINE,JSTD_NO_INLINE 这种宏,基本就没了。

shines77 commented 2 years ago

目前看 prefetch对hashmap 似乎没太大作用,即便rehash 过程线性遍历未必有多大收益。要配合循环展开特殊应用场景行 字符串hashmap提升性能有一定难度,相当部分消耗hash计算和内存分配(配合定制内存池和保留字符串, hash 可以提升性能)

没有,rehash 还是有收益的,你说的线性遍历是对于读的,或者某些线性的哈希函数,比如 gcc 下的 std::hash,std::hash,对于 Key = int, uint64_t 这种 POD 类型,如果能发现哈希函数是 gcc 下的 std::hash,std::hash,读和写都是线性的,甚至还可以用 std::memcpy 搬运 slot 元素(我没有这样做,虽然可以,好像我看到你的 hashmap 里有 memcpy,但不知道是不是用在这的)。但是,当我的 jstd::robin_hash_map 开启 hahs_policy,写 slot 就不是线性的了,或者不是使用 mask 取 index,用的 % 质数的方式(实现方式不一定是取余,可能用乘法优化),写也不是线性的了,所以还是有意义的。不过,就算是顺序的读和写,prefetch 还是有一些意义的,但提升会降低。

rehash 利用 prefetch 的方式是这样的,对于写,先批量取出一堆 slot,比如 128 个,计算他们的 key ,对应的哈希值和 index(并保存到数组中),得到将会可能写到的位置,用 prefetch 指令先写一下(不支持写就读一下),ARM上支持 prefetch write (gcc上支持)。然后利用已经计算出来的 hash 值和 index,开始搬迁旧 slot 到新 slot。

在我前面提到过的,我是看了这篇文章才写的 cardinal_becnh:HashTable性能测试(CK/phmap/ska)

这篇文章里面就提到了 prefetch 技术,虽然它没有具体的实现代码,但是你看这个测试代码,应该就能看得出来个大概:

static void run_insert_precompute(benchmark::State & state) {
    // Code inside this loop is measured repeatedly
    std::vector<int> a;
    ConstructRandomSet(state.range(0), state.range(1), a);

    static constexpr size_t PREFETCH = 16;
    std::vector<size_t> hash_values(BLOCK);

    for (auto _ : state) {
        HashSet<int> hs;
        const auto* data = a.data();
        const size_t size = a.size();

        for (size_t i = 0; i < size; i += BLOCK) {
            for (size_t j = 0; j < BLOCK; j++) {
                size_t hashval = hs.hash_function()(data[i + j]);
                hash_values[j] = hashval;
            }

            for (size_t j = 0, k = PREFETCH; j < BLOCK; j++, k++) {
                if (k < BLOCK) {
                    hs.prefetch_hash(hash_values[k]);
                }
                hs.emplace_with_hash(hash_values[j], data[i + j]);
            }
        }
        HSINFO(__func__);
    }
}

prefetch 的难点在于,提前量设置为多大,batch 长度取多大(对于写 prefetch,batch 的长度基本就等价于提前量,加上 pre_compute 的数据量,就是在给 CPU 去完成 prefetch 的 hint 。但是缺点就是,哈希值保存到数组,也是会进入缓存的,所以 prefetch 的提升必须要弥补这个损失,不然就会变成负优化,而且还徒增了很多本不需要的运算时间,也需要算在 prefetch 的成本里。

在这篇文章里,他说 prefetch 还是有效果的。(我才发现,其实你前面说的,可能已经是这篇文章,已经看到你在下面的回复了,没想到这个 blog 还是可以回复的啊。。。)

原来我说的,你都知道了的。。。

ktprime commented 2 years ago

https://github.com/greg7mdp/parallel-hashmap/blob/d62b8472a305359ced0417dd9e852e4488afb2dc/parallel_hashmap/phmap.h image

shines77 commented 2 years ago

你可以这么理解,他那篇文章是在插入的时候,使用了 prefetch 有收益。那么 rehash() 其实就是一个批量插入的过程,所以也就可以利用同样的 prefetch 技术了。

之前我跟你说过,在一个国产的叫 MatrixOne 的数据库(go语言写的),就使用了 batchInsert() 接口,他没有用 prefetch,但是他利用 SIMD 的 crc32 和 AES 指令,批量计算的时候,总体效率更高(crc32指令在 Intel 上的吞吐量是 1,延迟是 3,也就是说 3 条 crc32 指令连续执行,但只需要 5 个时钟周期。但 crc32指令在 AMD CPU上,吞吐量是 3,延迟是 3,没有任何优化价值),也是把哈希值先保存到数组,再批量插入,但他没做 prefetch 。

shines77 commented 2 years ago

另外,还有一个问题,最近我的香港的VPS IP好像被band了,我的科学上网出了点问题,github 上的图片,以 user-images.githubusercontent.com 域名开头的图片均访问不了,所以图片我都看不到。

ktprime commented 2 years ago

你可以看代码

   // Issues CPU prefetch instructions for the memory needed to find or insert
    // a key.  Like all lookup functions, this support heterogeneous keys.
    //
    // NOTE: This is a very low level operation and should not be used without
    // specific benchmarks indicating its importance.
    void prefetch_hash(size_t hashval) const {
        (void)hashval;
#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86))
        auto seq = probe(hashval);
        _mm_prefetch((const char *)(ctrl_ + seq.offset()), _MM_HINT_NTA);
        _mm_prefetch((const char *)(slots_ + seq.offset()), _MM_HINT_NTA);
#elif defined(__GNUC__)
        auto seq = probe(hashval);
        __builtin_prefetch(static_cast<const void*>(ctrl_ + seq.offset()));
        __builtin_prefetch(static_cast<const void*>(slots_ + seq.offset()));
#endif  // __GNUC__
    }

    template <class K = key_type>
    void prefetch(const key_arg<K>& key) const {
        prefetch_hash(this->hash(key));
    }
shines77 commented 2 years ago

他这个代码没什么特别的啊,基本是照抄 absl::flat_hash_map 的,就是利用 hash 值,计算出 ctrl 和 slot 的位置,读一下。其实 GCC 下的 __builtin_prefetch() 是支持 prefetch write 的,他没有利用这个特性,这会让 __builtin_prefetch() 在 ARM 等弱内存一致性的 CPU 上调用 prefetch write 指令,更准确一点。

我大致看了一下,这个 /parallel_hashmap/phmap.h,其实基本上是照抄的 absl::flat_hash_map,你基本上可以认为他是 absl::flat_hash_map 改进版,因为 absl::flat_hash_map 有这段相似的 prefetch 代码,但不是针对 hash 值的,并且也没有真正的用上。当然,也有可能是 absl::flat_hash_map 抄他的,不过我觉得可能性不太大。

absl::flat_hash_map 中的代码是这样的:

  // Issues CPU prefetch instructions for the memory needed to find or insert
  // a key.  Like all lookup functions, this support heterogeneous keys.
  //
  // NOTE: This is a very low level operation and should not be used without
  // specific benchmarks indicating its importance.
  template <class K = key_type>
  void prefetch(const key_arg<K>& key) const {
    (void)key;
    // Avoid probing if we won't be able to prefetch the addresses received.
#ifdef ABSL_INTERNAL_HAVE_PREFETCH
    prefetch_heap_block();
    auto seq = probe(ctrl_, hash_ref()(key), capacity_);
    base_internal::PrefetchT0(ctrl_ + seq.offset());
    base_internal::PrefetchT0(slots_ + seq.offset());
#endif  // ABSL_INTERNAL_HAVE_PREFETCH
  }

是不是很像?

shines77 commented 2 years ago

而且我发现一个错误,phmap.h 中 prefetch 的用法,在 MSVC 中的用法是错误的:

#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86))
        auto seq = probe(hashval);
        _mm_prefetch((const char *)(ctrl_ + seq.offset()), _MM_HINT_NTA);
        _mm_prefetch((const char *)(slots_ + seq.offset()), _MM_HINT_NTA);
#else
       ........
#endif

_MM_HINT_NTA 的意思是不经过缓存,使用 DMA 的方式(Directly Memory Access),直接写到内存,这不会有任何的 prefetch 效果。。。应该使用 _MM_HINT_T0,或者 _MM_HINT_T1,_MM_HINT_T2,一般使用 T0,因为既然我们要预取,肯定是尽量要预取到 L1 缓存中,如果提前量不够,可以加大 batch_size 。

shines77 commented 2 years ago

昨天下午,我已经把 jstd::robin_hash_map 的 bug 修复了,是一个地方的 swap(obj1, obj2); obj1, obj2 都是指针,所以应该写成 swap(obj1, obj2);

并且我也把 time_hash_map_new 移植到 hashmap_benchmark 中了,测了一下,结果出乎我的意料之外。

jstd::robin_hash_map 在原来的 time_hash_map 中测试 HashObject<size_t, 256, 64> 中(256Bytes 的大对象)是比 absl::flat_hasp_map 性能差很多的,但是在 time_hash_map_new 中测试 hashmap<std::string, std::string> 性能目前却是最好的,这还是未实现 https://github.com/martinus/unordered_dense 这种用索引指向 slot 数组的方式的情况下,如果实现了,性能会更好。

AMD EPYC 7K62 48核(2.6GHz,2核4G内存,Ubuntu 18.04,GCC 11.1)

hashmap<std::string, std::string> benchmark:(Key是 5-31 个字符的随机字符串,Value 是 1-31 字符的随机字符串)

jstd::robin_hash_map<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                139.90 ns  lf=0.298  
random_find_success                177.33 ns  lf=0.298  
find_failed                         46.83 ns  lf=0.298  
find_empty                          48.97 ns  lf=0.000  

insert                             527.94 ns  lf=0.298    300.4 MB (+128.0 MB)
insert_predicted                   386.38 ns  lf=0.298    300.4 MB (+127.9 MB)
insert_replace                     160.84 ns  lf=0.298    428.4 MB

emplace                            540.46 ns  lf=0.298    300.4 MB (+128.0 MB)
emplace_predicted                  381.43 ns  lf=0.298    300.4 MB (+127.9 MB)
emplace_replace                    165.11 ns  lf=0.298    428.4 MB

try_emplace                        544.64 ns  lf=0.298    300.4 MB (+128.0 MB)
try_emplace_predicted              385.99 ns  lf=0.298    300.4 MB (+127.9 MB)
try_emplace_replace                195.46 ns  lf=0.298    428.4 MB

operator []                        546.46 ns  lf=0.298    300.4 MB (+128.0 MB)
operator [] predicted              386.87 ns  lf=0.298    300.4 MB (+127.9 MB)
operator [] replace                206.42 ns  lf=0.298    428.4 MB

serial_erase                       234.43 ns  lf=0.000    428.4 MB
random_erase                       292.05 ns  lf=0.000    428.4 MB
erase_failed                        50.67 ns  lf=0.298    428.4 MB

toggle                             106.22 ns  lf=0.000    300.4 MB
iterate                             19.06 ns  lf=0.298    428.4 MB
ska::flat_hash_map<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                137.20 ns  lf=0.298  
random_find_success                173.51 ns  lf=0.298  
find_failed                         93.48 ns  lf=0.298  
find_empty                          83.25 ns  lf=0.000  

insert                             732.67 ns  lf=0.298    303.4 MB (+144.0 MB)
insert_predicted                   490.44 ns  lf=0.298    303.4 MB (+144.0 MB)
insert_replace                     141.13 ns  lf=0.298    447.4 MB

emplace                            725.69 ns  lf=0.298    303.4 MB (+144.0 MB)
emplace_predicted                  489.52 ns  lf=0.298    303.4 MB (+144.0 MB)
emplace_replace                    143.46 ns  lf=0.298    447.4 MB

try_emplace                        745.67 ns  lf=0.298    303.4 MB (+144.0 MB)
try_emplace_predicted              501.55 ns  lf=0.298    303.4 MB (+144.0 MB)
try_emplace_replace                179.92 ns  lf=0.298    447.4 MB

operator []                        741.91 ns  lf=0.298    303.4 MB (+144.0 MB)
operator [] predicted              504.52 ns  lf=0.298    303.4 MB (+144.0 MB)
operator [] replace                179.20 ns  lf=0.298    447.4 MB

serial_erase                       231.00 ns  lf=0.000    447.4 MB
random_erase                       278.45 ns  lf=0.000    447.4 MB
erase_failed                        87.53 ns  lf=0.298    447.4 MB

toggle                              96.52 ns  lf=0.000    303.4 MB
iterate                             16.59 ns  lf=0.298    447.4 MB
emhash5::HashMap<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                140.68 ns  lf=0.596  
random_find_success                175.08 ns  lf=0.596  
find_failed                         95.09 ns  lf=0.596  
find_empty                          74.65 ns  lf=0.000  

insert                             676.71 ns  lf=0.596    303.4 MB (+72.0 MB)
insert_predicted                   375.84 ns  lf=0.596    303.4 MB (+71.9 MB)
insert_replace                     163.44 ns  lf=0.596    375.4 MB

emplace                            666.75 ns  lf=0.596    303.4 MB (+72.0 MB)
emplace_predicted                  381.48 ns  lf=0.596    303.4 MB (+71.9 MB)
emplace_replace                    167.82 ns  lf=0.596    375.4 MB

try_emplace                        724.13 ns  lf=0.596    303.4 MB (+72.0 MB)
try_emplace_predicted              377.04 ns  lf=0.596    303.4 MB (+71.9 MB)
try_emplace_replace                200.57 ns  lf=0.596    375.4 MB

operator []                        694.53 ns  lf=0.596    303.4 MB (+72.0 MB)
operator [] predicted              393.05 ns  lf=0.596    303.4 MB (+71.9 MB)
operator [] replace                217.85 ns  lf=0.596    375.4 MB

serial_erase                       244.54 ns  lf=0.000    375.4 MB
random_erase                       279.51 ns  lf=0.000    375.4 MB
erase_failed                        94.86 ns  lf=0.596    375.4 MB

toggle                              94.48 ns  lf=0.000    303.4 MB
iterate                              9.65 ns  lf=0.596    375.4 MB
absl::flat_hash_map<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                214.47 ns  lf=0.596  
random_find_success                259.89 ns  lf=0.596  
find_failed                         75.84 ns  lf=0.596  
find_empty                          65.41 ns  lf=0.000  

insert                             690.03 ns  lf=0.596    303.4 MB (+65.0 MB)
insert_predicted                   400.56 ns  lf=0.596    303.4 MB (+65.0 MB)
insert_replace                     236.83 ns  lf=0.596    368.4 MB

emplace                            692.79 ns  lf=0.596    303.4 MB (+65.0 MB)
emplace_predicted                  399.18 ns  lf=0.596    303.4 MB (+65.0 MB)
emplace_replace                    239.47 ns  lf=0.596    368.4 MB

try_emplace                        695.86 ns  lf=0.596    303.4 MB (+65.0 MB)
try_emplace_predicted              406.26 ns  lf=0.596    303.4 MB (+65.0 MB)
try_emplace_replace                284.39 ns  lf=0.596    368.4 MB

operator []                        698.35 ns  lf=0.596    303.4 MB (+65.0 MB)
operator [] predicted              411.84 ns  lf=0.596    303.4 MB (+65.0 MB)
operator [] replace                269.57 ns  lf=0.596    368.4 MB

serial_erase                       304.86 ns  lf=0.000    368.4 MB
random_erase                       367.97 ns  lf=0.000    368.4 MB
erase_failed                        76.40 ns  lf=0.596    368.4 MB

toggle                             200.27 ns  lf=0.000    303.4 MB
iterate                              6.12 ns  lf=0.596    368.4 MB
ktprime commented 2 years ago

测试使用emhash8(针对大 key+value),emhash5针对小数据,ska和你的hashmap load_factor 小一点性能要好一些 可以更新我的git同步一下代码我也测试看看

shines77 commented 2 years ago

hashmap_benchmark 我新加了三个哈希表:

time_hash_map_new 中也加入以上3个哈希表,以及 emhash8 的测试,emhash8jstd::robin_hash_map 差不多,ankerl::unordered_dense::map 毫无疑问现在是最快的,我已经在写使用 index 的版本了,不过复杂度不低,把所有逻辑揉在一起工作量有点高,单独写要简单一些,但是写一个完全自适应的版本也是我的初衷,C++版的目前我还没看到过。如果要说有, go 语言的 map 算是,但是 go 语言的版本我觉得写得不够好,而且 go 的编译器跟 C++ 还有点差距。

如果只想测试 <std::string, std::string>,可以使用这样的命令行参数,节约时间一点:

./bin/time_hash_map_new string

测试结果:

AMD EPYC 7K62 48核(2.6GHz,2核4G内存,Ubuntu 18.04,GCC 11.1)

jstd::robin_hash_map<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                145.39 ns  lf=0.298  
random_find_success                174.25 ns  lf=0.298  
find_failed                         52.15 ns  lf=0.298  
find_empty                          48.91 ns  lf=0.000  

insert                             539.48 ns  lf=0.298    300.5 MB (+128.0 MB)
insert_predicted                   380.29 ns  lf=0.298    300.5 MB (+127.9 MB)
insert_replace                     165.20 ns  lf=0.298    428.5 MB

emplace                            540.17 ns  lf=0.298    300.5 MB (+128.0 MB)
emplace_predicted                  383.57 ns  lf=0.298    300.5 MB (+127.9 MB)
emplace_replace                    166.11 ns  lf=0.298    428.5 MB

try_emplace                        533.87 ns  lf=0.298    300.5 MB (+128.0 MB)
try_emplace_predicted              371.17 ns  lf=0.298    300.5 MB (+127.9 MB)
try_emplace_replace                199.15 ns  lf=0.298    428.5 MB

operator []                        529.49 ns  lf=0.298    300.5 MB (+128.0 MB)
operator [] predicted              379.41 ns  lf=0.298    300.5 MB (+127.9 MB)
operator [] replace                205.80 ns  lf=0.298    428.5 MB

serial_erase                       248.11 ns  lf=0.000    428.5 MB
random_erase                       288.34 ns  lf=0.000    428.5 MB
erase_failed                        43.40 ns  lf=0.298    428.5 MB

toggle                              99.15 ns  lf=0.000    300.5 MB
iterate                             19.60 ns  lf=0.298    428.5 MB
emhash8::HashMap<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                120.61 ns  lf=0.596  
random_find_success                221.26 ns  lf=0.596  
find_failed                         77.87 ns  lf=0.596  
find_empty                          63.29 ns  lf=0.000  

insert                             568.12 ns  lf=0.596    321.3 MB (+37.9 MB)
insert_predicted                   389.20 ns  lf=0.596    321.3 MB (+37.9 MB)
insert_replace                     179.74 ns  lf=0.596    359.2 MB

emplace                            570.37 ns  lf=0.596    321.3 MB (+37.9 MB)
emplace_predicted                  391.85 ns  lf=0.596    321.3 MB (+37.9 MB)
emplace_replace                    174.36 ns  lf=0.596    359.2 MB

try_emplace                        565.84 ns  lf=0.596    321.3 MB (+37.9 MB)
try_emplace_predicted              389.64 ns  lf=0.596    321.3 MB (+37.9 MB)
try_emplace_replace                291.43 ns  lf=0.596    359.2 MB

operator []                        567.85 ns  lf=0.596    321.3 MB (+37.9 MB)
operator [] predicted              387.62 ns  lf=0.596    321.3 MB (+37.9 MB)
operator [] replace                215.08 ns  lf=0.596    359.2 MB

serial_erase                       235.95 ns  lf=0.000    359.2 MB
random_erase                       443.07 ns  lf=0.000    359.2 MB
erase_failed                        62.27 ns  lf=0.596    359.2 MB

toggle                              99.63 ns  lf=0.000    321.3 MB
iterate                              3.57 ns  lf=0.596    359.2 MB
ankerl::unordered_dense::map<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                113.47 ns  lf=0.596  
random_find_success                195.19 ns  lf=0.596  
find_failed                         41.23 ns  lf=0.596  
find_empty                           2.83 ns  lf=0.000  

insert                             378.61 ns  lf=0.596    303.7 MB (+37.9 MB)
insert_predicted                   333.75 ns  lf=0.596    303.7 MB (+42.9 MB)
insert_replace                     225.93 ns  lf=0.596    346.6 MB

emplace                            465.11 ns  lf=0.596    308.7 MB (+37.9 MB)
emplace_predicted                  331.37 ns  lf=0.596    308.7 MB (+37.9 MB)
emplace_replace                    243.27 ns  lf=0.596    346.6 MB

try_emplace                        468.07 ns  lf=0.596    308.7 MB (+37.9 MB)
try_emplace_predicted              331.42 ns  lf=0.596    308.7 MB (+37.9 MB)
try_emplace_replace                411.04 ns  lf=0.596    346.6 MB

operator []                        460.16 ns  lf=0.596    308.7 MB (+37.9 MB)
operator [] predicted              337.43 ns  lf=0.596    308.7 MB (+37.9 MB)
operator [] replace                141.90 ns  lf=0.596    346.6 MB

serial_erase                       217.92 ns  lf=0.000    346.6 MB
random_erase                       455.44 ns  lf=0.000    346.6 MB
erase_failed                        44.21 ns  lf=0.596    346.6 MB

toggle                              90.80 ns  lf=0.000    308.7 MB
iterate                              3.59 ns  lf=0.596    346.6 MB
shines77 commented 2 years ago

tsl::robin_map, robin_hood::unordered_flat_map 在测试 <std::string, std::string> 时性能一般,我就不贴了,有兴趣你可以测一下。

ktprime commented 2 years ago

测试数据按hashmap 横向对比输出比较直观(需预先存,按接口输出), load factor 是影响性能的重要因素,可以设置相同的最大 max_load_factor 再对比(我改其他hashmap 设置相同load_factor 对比 按absl 默认7/8), 换一个编译器在其他cpu测试结果也可能不同

ktprime commented 2 years ago

intel 9700 clang 12 刚才把你hash map 负载设置0.8 测试卡主不动 你的实现不错加油小伙子


jstd::robin_hash_map<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                 90.50 ns  lf=0.298
random_find_success                122.97 ns  lf=0.298
find_failed                         29.75 ns  lf=0.298
find_empty                          28.50 ns  lf=0.000

insert                             378.09 ns  lf=0.298    300.8 MB (+128.0 MB)
insert_predicted                   237.47 ns  lf=0.298    300.8 MB (+127.8 MB)
insert_replace                      93.04 ns  lf=0.298    428.8 MB

emplace                            436.86 ns  lf=0.298    300.8 MB (+128.0 MB)
emplace_predicted                  239.01 ns  lf=0.298    300.8 MB (+127.8 MB)
emplace_replace                     95.98 ns  lf=0.298    428.8 MB

try_emplace                        375.62 ns  lf=0.298    300.8 MB (+128.0 MB)
try_emplace_predicted              228.17 ns  lf=0.298    300.8 MB (+127.8 MB)
try_emplace_replace                128.47 ns  lf=0.298    428.8 MB

operator []                        464.22 ns  lf=0.298    300.8 MB (+128.1 MB)
operator [] predicted              309.80 ns  lf=0.298    300.9 MB (+127.8 MB)
operator [] replace                179.69 ns  lf=0.298    428.9 MB

serial_erase                       157.19 ns  lf=0.000    428.9 MB
random_erase                       239.81 ns  lf=0.000    428.9 MB
erase_failed                        27.76 ns  lf=0.298    428.9 MB

toggle                              77.09 ns  lf=0.000    300.9 MB
iterate                              9.97 ns  lf=0.298    428.9 MB

emhash8::HashMap<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                 74.75 ns  lf=0.596
random_find_success                141.49 ns  lf=0.596
find_failed                         41.38 ns  lf=0.596
find_empty                          31.49 ns  lf=0.000

insert                             441.69 ns  lf=0.596    321.7 MB (+38.0 MB)
insert_predicted                   285.13 ns  lf=0.596    321.7 MB (+38.0 MB)
insert_replace                     114.39 ns  lf=0.596    359.7 MB

emplace                            467.85 ns  lf=0.596    321.7 MB (+38.0 MB)
emplace_predicted                  277.92 ns  lf=0.596    321.7 MB (+38.0 MB)
emplace_replace                    117.26 ns  lf=0.596    359.7 MB

try_emplace                        447.40 ns  lf=0.596    321.7 MB (+38.0 MB)
try_emplace_predicted              273.30 ns  lf=0.596    321.7 MB (+38.0 MB)
try_emplace_replace                179.50 ns  lf=0.596    359.7 MB

operator []                        500.93 ns  lf=0.596    321.7 MB (+38.0 MB)
operator [] predicted              331.30 ns  lf=0.596    321.7 MB (+38.0 MB)
operator [] replace                182.84 ns  lf=0.596    359.7 MB

serial_erase                       156.04 ns  lf=0.000    359.7 MB
random_erase                       314.11 ns  lf=0.000    359.7 MB
erase_failed                        42.15 ns  lf=0.596    359.7 MB

toggle                              78.80 ns  lf=0.000    321.7 MB
iterate                              2.91 ns  lf=0.596    359.7 MB

ankerl::unordered_dense::map<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                 74.19 ns  lf=0.596
random_find_success                130.56 ns  lf=0.596
find_failed                         30.59 ns  lf=0.596
find_empty                           1.76 ns  lf=0.000

insert                             336.10 ns  lf=0.596    321.7 MB (+39.0 MB)
insert_predicted                   237.11 ns  lf=0.596    321.7 MB (+39.0 MB)
insert_replace                     170.13 ns  lf=0.596    360.6 MB

emplace                            419.15 ns  lf=0.596    321.7 MB (+39.0 MB)
emplace_predicted                  248.06 ns  lf=0.596    321.7 MB (+39.0 MB)
emplace_replace                    158.37 ns  lf=0.596    360.6 MB

try_emplace                        401.64 ns  lf=0.596    321.7 MB (+39.0 MB)
try_emplace_predicted              244.17 ns  lf=0.596    321.7 MB (+39.0 MB)
try_emplace_replace                291.25 ns  lf=0.596    360.6 MB

operator []                        477.34 ns  lf=0.596    321.7 MB (+39.0 MB)
operator [] predicted              323.91 ns  lf=0.596    321.7 MB (+39.0 MB)
operator [] replace                146.78 ns  lf=0.596    360.6 MB

serial_erase                       175.89 ns  lf=0.000    360.6 MB
random_erase                       360.18 ns  lf=0.000    360.6 MB
erase_failed                        33.99 ns  lf=0.596    360.6 MB

toggle                              68.56 ns  lf=0.000    321.7 MB
iterate                              3.07 ns  lf=0.596    360.6 MB

-----------------------------------------------------------------------------
shines77 commented 2 years ago

改成向你那样的表格横向比较是不错,我一直想改成那样的,但一种没动手,思想上准备了的,我有在清理相关的接口。

提到 load_factor,我很早就想跟你说的,你很早也跟我提过,为了公平,要统一 load_factor 来测,我是有不同意见的。如果我没有写过 robin hood 哈希表,可能我会赞同你的意见,但是当我写了 robin hood 哈希表以后,才知道统一 load_factor 才是不公平的。我是因为想整理我几年前写的 jstd::Dictionay(拉链法+内置内存池+保存完整hash), jstd::Dictionay 在数据总量很小的时候,很快,因为所有 entry 分配一次以后,就不需要在移动,包括 rehash() 的时候,也不需要再做 hash,但是缺点嘛,也很明显,就是缓存使用量是很大的,高负载下,性能就不行了,我没看 aka::flat_hash_map 的视频之前是不知道这些事情的。

我是想搜索看看,还有没有更好的哈希表资料,无意中,我看到了 aka::flat_hash_map 作者在 CppNow2018 上的演讲视频,然后又找到了 Google 在 CppCon 2017 年的 Swiss Table 演讲视频,有点可惜的是,2019年前后,我写 jstd::Dictionay 没有搜索到这些视频,浪费了不少时间来写 jstd::Dictionay 。

aka::flat_hash_map 作者最伟大的发现,可能就是他发现 robin hood 哈希表 max_load_factor 设置在 0.5 左右,性能会更好,这看似简单的设置,还是非常有用的,针对 robin hood 哈希表 distance 不能太大(最大128),他还发明了 hash_policy,非常巧妙的解决了在同一个入口,distance 可能过大的问题。他还有不少神来之笔,我的 jstd::robin_hash_map 基本上是在他的基础上改进而来的。

这些不是重点,重点是 max_load_factor 设为 0.5,我试过,改大了,直接就卡死了,我现在是没启用 hash_policy,启用了之后,max_load_factor 可能可以设置得比 0.5 稍微大一点,但还是不能设置得太大,大了性能可能会打折扣。

我甚至在我自己写的 Swiss Table 版本 jstd::flat16_hash_map 也是设置为 mlf 0.5 的,所以这让我意识到,有的哈希表天生就不能支持太高的 load_factor,absl::flat_hash_map 其实在 load_factor 高了性能也会下降的,但 Google 可能太看重内存占用了, 给它加了一个 slot 整理,勉强支撑起较高的 mlf。所以每个哈希表都有其自己的特点,不必强求一定要统一,像拉链法,或者布谷鸟哈希表,或者类似链表法的哈希表,都可以做到很高的 load_factor,不瞒你说,我的 jstd::Directionay 设置的也是 1.0 的 mlf。

正是因为不同的哈希表方法,导致了不同的实现有它独特的特性,robin hood 和 Swiss Table 都很依赖空白位,空白位太少,或者连续的非空白位太多,性能就会下降很多,所以可能就是这些哈希表需要设置比较小的 mlf 的原因。

所以,不统一 load_factor 也没关系,load_factor 小,浪费的内存会多一点,但这也不代表最终的性能就会更慢,高负载下,决定性能的因素主要还是缓存局部性,污染的缓存行的总数量越小,性能可能就越好。

但是,从另一方面,也应该有另一个比赛项目,那就是在相同的 load_factor 的情况下,谁的性能最高,因为有的用户可能对内存的占用比较敏感,每种哈希表都有他的特点,优点和缺点,没有永远的银弹,这也是我想写文章来说说的原因。哈希表还有不少其他类型,有的我也还没有尝试过,不过在主流的实现中,目前也就基本这几个比较常见。

其实,我也看出来了,你的 emhash8 应该和 ankerl::unordered_dense::map 相似,用的索引,以及 entry 数据是保存在连续的数组里的,因为你们的内存使用量都比我 jstd::robin_hash_map 要小,而且你们内存使用量很接近。

ktprime commented 2 years ago

早期emhash7 默认使用0.95 负载因子,插入性能影响很小(大多场景插入、rehash 比查询还是偏少),查询性能会慢20%左右,数据上看有性能损失但节省更多内存。unordered_dense 在高负载下性能比我的emhash8略差一些。 如果对性能极其敏感可以设置低一些,追求性能和内存平衡是一个不错的选择

ktprime commented 2 years ago

测试场景喜欢使用大数据,实际场景并不多见,真正大map数据分布很少随机,把小hash map数据负载做高还是有好处。之前emhash7 元素低于64个满了才rehash,性能也不差,你可以考虑加一个大量小hashmap的测试

template<class hash_type>
void multi_small_ife(const std::string& hash_name, const std::vector<keyType>& vList)
{
#if KEY_INT
    size_t sum = 0;
    const auto hash_size = vList.size() / 1003 + 10;
    const auto ts1 = getus();

    if (test_case % 2) {
        auto mh = new hash_type[hash_size];
        for (const auto& v : vList) {
            auto hash_id = ((uint32_t)v) % hash_size;
            sum += mh[hash_id].emplace(v, TO_VAL(0)).second;
        }

        for (const auto& v : vList) {
            auto hash_id = ((uint32_t)v) % hash_size;
            sum += mh[hash_id].count(v);
        }

        for (const auto& v : vList) {
            auto hash_id = ((uint32_t)v) % hash_size;
            sum += mh[hash_id].erase(v + v % 2);
        }

        delete []mh;
    } else {
        hash_type hashm;
        for (const auto v : vList) {
            const keyType v2 = v % hash_size;
            sum += hashm.emplace(v2, TO_VAL(0)).second;
            sum += hashm.erase(v2 + v % 2);
            sum += hashm.count(v2 / 2);
        }
    }

    check_func_result(hash_name, __FUNCTION__, sum, ts1, 2);
#endif
}
ktprime commented 2 years ago

5800H + clang11 , 结果有些差异

jstd::robin_hash_map<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                113.65 ns  lf=0.298
random_find_success                125.02 ns  lf=0.298
find_failed                         30.00 ns  lf=0.298
find_empty                          34.74 ns  lf=0.000

insert                             539.09 ns  lf=0.298    298.5 MB (+128.0 MB)
insert_predicted                   315.05 ns  lf=0.298    298.5 MB (+128.0 MB)
insert_replace                     100.72 ns  lf=0.298    426.5 MB

emplace                            547.90 ns  lf=0.298    298.6 MB (+128.0 MB)
emplace_predicted                  309.82 ns  lf=0.298    298.6 MB (+128.0 MB)
emplace_replace                    101.00 ns  lf=0.298    426.6 MB

try_emplace                        535.15 ns  lf=0.298    298.6 MB (+128.0 MB)
try_emplace_predicted              326.18 ns  lf=0.298    298.6 MB (+128.0 MB)
try_emplace_replace                117.61 ns  lf=0.298    426.6 MB

operator []                        556.78 ns  lf=0.298    298.6 MB (+128.0 MB)
operator [] predicted              325.17 ns  lf=0.298    298.6 MB (+128.0 MB)
operator [] replace                139.05 ns  lf=0.298    426.6 MB

serial_erase                       180.30 ns  lf=0.000    426.6 MB
random_erase                       228.70 ns  lf=0.000    426.6 MB
erase_failed                        27.50 ns  lf=0.298    426.6 MB

toggle                              53.09 ns  lf=0.000    298.6 MB
iterate                              9.79 ns  lf=0.298    426.6 MB (+0.0 MB)

emhash8::HashMap<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                 81.93 ns  lf=0.596
random_find_success                129.17 ns  lf=0.596
find_failed                         36.67 ns  lf=0.596
find_empty                          40.98 ns  lf=0.000

insert                             407.42 ns  lf=0.596    319.4 MB (+38.2 MB)
insert_predicted                   270.59 ns  lf=0.596    319.4 MB (+38.1 MB)
insert_replace                     117.56 ns  lf=0.596    357.5 MB

emplace                            409.24 ns  lf=0.596    319.4 MB (+38.1 MB)
emplace_predicted                  275.17 ns  lf=0.596    319.4 MB (+38.1 MB)
emplace_replace                    123.55 ns  lf=0.596    357.5 MB

try_emplace                        400.32 ns  lf=0.596    319.4 MB (+38.1 MB)
try_emplace_predicted              272.28 ns  lf=0.596    319.4 MB (+38.1 MB)
try_emplace_replace                177.69 ns  lf=0.596    357.5 MB

operator []                        398.24 ns  lf=0.596    319.4 MB (+38.1 MB)
operator [] predicted              282.72 ns  lf=0.596    319.4 MB (+38.1 MB)
operator [] replace                133.60 ns  lf=0.596    357.5 MB

serial_erase                       159.30 ns  lf=0.000    357.5 MB
random_erase                       299.67 ns  lf=0.000    357.5 MB
erase_failed                        38.56 ns  lf=0.596    357.5 MB

toggle                              53.85 ns  lf=0.000    319.4 MB
iterate                              2.82 ns  lf=0.596    357.5 MB

ankerl::unordered_dense::map<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                 77.12 ns  lf=0.596
random_find_success                131.86 ns  lf=0.596
find_failed                         26.88 ns  lf=0.596
find_empty                           1.65 ns  lf=0.000

insert                             308.24 ns  lf=0.596    319.4 MB (+38.1 MB)
insert_predicted                   268.59 ns  lf=0.596    319.4 MB (+38.1 MB)
insert_replace                     124.81 ns  lf=0.596    357.5 MB

emplace                            313.85 ns  lf=0.596    319.4 MB (+38.1 MB)
emplace_predicted                  268.45 ns  lf=0.596    319.4 MB (+38.1 MB)
emplace_replace                    124.93 ns  lf=0.596    357.5 MB

try_emplace                        313.21 ns  lf=0.596    319.4 MB (+38.1 MB)
try_emplace_predicted              264.79 ns  lf=0.596    319.4 MB (+38.1 MB)
try_emplace_replace                276.97 ns  lf=0.596    357.5 MB

operator []                        317.05 ns  lf=0.596    319.4 MB (+38.1 MB)
operator [] predicted              271.61 ns  lf=0.596    319.4 MB (+38.1 MB)
operator [] replace                 84.33 ns  lf=0.596    357.5 MB

serial_erase                       167.31 ns  lf=0.000    357.5 MB
random_erase                       333.89 ns  lf=0.000    357.5 MB
erase_failed                        28.45 ns  lf=0.596    357.5 MB

toggle                              40.94 ns  lf=0.000    319.4 MB
iterate                              2.71 ns  lf=0.596    357.5 MB

-----------------------------------------------------------------------------
shines77 commented 2 years ago

刚才睡着了,看来还是有差距的,只不过 AMD EPYC 7K62 48核 的 L3 缓存比较大,没拉开差距。Intel 9700 可能是缓存的性能也不错,主频和内存频率也高,所以也没看出差距。

ktprime commented 2 years ago

如果在arm,M1跑结果大不一样,看是很好的优化可能不一定有效果,手头目前没有机器,我的github主页有之前在华为鲲鹏 arm和m1跑的对比结果。

cpu + 缓存 + 编译器组合会产生不同测试结果。大多benchmark使用一组随机数据,而我的ebench采用500组随机case(load factor,hash, 随机生成器, key & value 多种组合, os,编译器)加权计算各种 hash map不同接口和整体综合 相对性能(对比使用绝对数值xxx us价值 不大 ) martin 的测试case很好,但他只在intel 8700测试得出结论不那么让人信服,至少我在服务器跑过结论有些不一样。

ktprime commented 2 years ago

define USE_JSTD_ROBIN_HASH_MAP 0 //编译出错

大数据 robin_hood::unordered_map(node_map) 比 robin_hood::unordered_flat_map 快。

if USE_ROBIN_HOOD_FLAT_MAP

if (1) {
    measure_string_hashmap<robin_hood::unordered_map<Key, Value>>
        ("robin_hood::unordered_map<K, V>", obj_size, iters);
}

endif

robin_hood::unordered_map<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                 87.23 ns  lf=0.596
random_find_success                148.24 ns  lf=0.596
find_failed                         29.43 ns  lf=0.596
find_empty                          34.72 ns  lf=0.000

insert                             339.62 ns  lf=0.596    319.4 MB
insert_predicted                   206.10 ns  lf=0.596    319.4 MB
insert_replace                     161.67 ns  lf=0.596    319.4 MB

emplace                            340.25 ns  lf=0.596    319.4 MB
emplace_predicted                  202.33 ns  lf=0.596    319.4 MB
emplace_replace                    164.97 ns  lf=0.596    319.4 MB

try_emplace                        332.87 ns  lf=0.596    319.4 MB
try_emplace_predicted              199.72 ns  lf=0.596    319.4 MB
try_emplace_replace                338.48 ns  lf=0.596    319.4 MB

operator []                        336.36 ns  lf=0.596    319.4 MB (+0.0 MB)
operator [] predicted              206.55 ns  lf=0.596    319.4 MB
operator [] replace                115.38 ns  lf=0.596    319.4 MB

serial_erase                       160.33 ns  lf=0.000    319.4 MB
random_erase                       294.65 ns  lf=0.000    319.4 MB
erase_failed                        29.45 ns  lf=0.596    319.4 MB

toggle                              60.02 ns  lf=0.000    319.4 MB
iterate                             10.55 ns  lf=0.596    319.4 MB

ankerl::unordered_dense::map<K, V> (32 byte objects, 64 byte ValueType, 625000 iterations):

serial_find_success                 87.65 ns  lf=0.596
random_find_success                138.70 ns  lf=0.596
find_failed                         33.55 ns  lf=0.596
find_empty                           1.64 ns  lf=0.000

insert                             320.50 ns  lf=0.596    319.4 MB (+38.1 MB)
insert_predicted                   265.44 ns  lf=0.596    319.4 MB (+38.1 MB)
insert_replace                     131.12 ns  lf=0.596    357.6 MB

emplace                            306.81 ns  lf=0.596    319.4 MB (+38.1 MB)
emplace_predicted                  266.15 ns  lf=0.596    319.4 MB (+38.1 MB)
emplace_replace                    149.73 ns  lf=0.596    357.6 MB

try_emplace                        314.28 ns  lf=0.596    319.4 MB (+38.1 MB)
try_emplace_predicted              278.55 ns  lf=0.596    319.4 MB (+38.1 MB)
try_emplace_replace                291.62 ns  lf=0.596    357.6 MB

operator []                        307.71 ns  lf=0.596    319.4 MB (+38.1 MB)
operator [] predicted              278.75 ns  lf=0.596    319.4 MB (+38.1 MB)
operator [] replace                 86.23 ns  lf=0.596    357.6 MB

serial_erase                       160.52 ns  lf=0.000    357.6 MB
random_erase                       324.57 ns  lf=0.000    357.6 MB
erase_failed                        29.51 ns  lf=0.596    357.6 MB

toggle                              39.32 ns  lf=0.000    319.4 MB
iterate                              2.60 ns  lf=0.596    357.6 MB

-----------------------------------------------------------------------------
shines77 commented 2 years ago

我测试了一下,USE_JSTD_ROBIN_HASH_MAP 倒没发现编译错误,但由于早上我改了一下 aka::flat_hash_map,倒是有个编译错误,已经修正。

更新 submodule 建议使用:

# 更新到仓库指定的版本,而不是最新版本
git submodule update --init --recursive

不要使用 git submodule update --remote --recursive ,因为这句会更新到 submodule 的最新版本,可能由于还在开发中或者版本不兼容导致编译错误,或者运行错误。

jstd_hashmap 我就懒得切换到 develop 分支里修改了,直接就是在 master 分支里修改的,所以最新版不一定能编译通过,或者保证运行结果是正确的。我测试好了,再更新到 hashmap_benchmark 里,所以使用上面的第一条命令更新 submodule 即可。

另外,我改成 robin_hood::unordered_map 了,这是一个自适应版本,会自动切换 flat_map 和 node_map 。