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 以及查找命中不错性能 , 删除比较慢

ktprime commented 2 years ago

rigtorp::HashMap 目前是我测试过插入性能最快的hash map, 最大缺陷是需要一个默认空值(不能插入,有些接口不符合stl),查询miss 性能比较差。

shines77 commented 2 years ago

看了一下,rigtorp::HashMap 的优点在于,省掉了 metadata,用 Empty_Key 代替 metadata,如果是刚好一个萝卜一个坑,没有哈希冲突的时候,性能还是不错的,但是如果一旦冲突多了,且空值位离得比较远,性能就会降低,如果运气不好,性能可能会急速下降。总的来说,这样的哈希表过于简单,不太可能应用在实际环境中。

所以,我之前说我的 time_hash_map 的 random 测试,其实只是顺序是打乱的,本质上还是一个萝卜一个坑,这对于测试哈希冲突并没有意义,我本是想打算把 time_hash_map 做为偏理论上的测试,现在看来还是要偏实际更有意义一点,serail (顺序)的测试已经算是很理论上的测试了。

ktprime commented 2 years ago

image

服务器 gcc 9.4 Intel(R) Xeon(R) Gold 6271C CPU @ 2.60GHz

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

serial_find_success                129.33 ns  lf=0.298  
random_find_success                156.75 ns  lf=0.298  
find_failed                         53.28 ns  lf=0.298  
find_empty                          47.32 ns  lf=0.000  

insert                             597.77 ns  lf=0.298    297.4 MB (+128.0 MB)
insert_predicted                   392.40 ns  lf=0.298    297.4 MB (+127.8 MB)
insert_replace                     158.06 ns  lf=0.298    425.4 MB

emplace                            599.65 ns  lf=0.298    297.4 MB (+128.0 MB)
emplace_predicted                  467.37 ns  lf=0.298    297.4 MB (+127.8 MB)
emplace_replace                    172.36 ns  lf=0.298    425.4 MB

try_emplace                        628.01 ns  lf=0.298    297.4 MB (+128.0 MB)
try_emplace_predicted              412.06 ns  lf=0.298    297.4 MB (+127.8 MB)
try_emplace_replace                176.48 ns  lf=0.298    425.4 MB

operator []                        633.08 ns  lf=0.298    297.4 MB (+128.0 MB)
operator [] predicted              421.25 ns  lf=0.298    297.4 MB (+127.8 MB)
operator [] replace                229.74 ns  lf=0.298    425.4 MB

serial_erase                       232.15 ns  lf=0.000    425.4 MB
random_erase                       294.37 ns  lf=0.000    425.4 MB
erase_failed                        47.39 ns  lf=0.298    425.4 MB

toggle                             135.59 ns  lf=0.000    297.4 MB
iterate                             17.42 ns  lf=0.298    425.4 MB

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

serial_find_success                119.15 ns  lf=0.596  
random_find_success                182.98 ns  lf=0.596  
find_failed                         58.36 ns  lf=0.596  
find_empty                          42.29 ns  lf=0.000  

insert                             557.16 ns  lf=0.596    319.8 MB (+38.7 MB)
insert_predicted                   353.42 ns  lf=0.596    319.8 MB (+38.7 MB)
insert_replace                     190.98 ns  lf=0.596    358.4 MB

emplace                            546.60 ns  lf=0.596    319.8 MB (+38.7 MB)
emplace_predicted                  365.33 ns  lf=0.596    319.8 MB (+38.7 MB)
emplace_replace                    184.87 ns  lf=0.596    358.4 MB

try_emplace                        569.10 ns  lf=0.596    319.8 MB (+38.7 MB)
try_emplace_predicted              358.00 ns  lf=0.596    319.8 MB (+38.7 MB)
try_emplace_replace                230.68 ns  lf=0.596    358.4 MB

operator []                        554.89 ns  lf=0.596    319.8 MB (+38.7 MB)
operator [] predicted              384.51 ns  lf=0.596    319.8 MB (+38.7 MB)
operator [] replace                199.97 ns  lf=0.596    358.4 MB

serial_erase                       263.69 ns  lf=0.000    358.4 MB
random_erase                       409.19 ns  lf=0.000    358.4 MB
erase_failed                        58.14 ns  lf=0.596    358.4 MB

toggle                             127.55 ns  lf=0.000    319.8 MB
iterate                              4.73 ns  lf=0.596    358.4 MB

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

serial_find_success                135.62 ns  lf=0.596  
random_find_success                200.06 ns  lf=0.596  
find_failed                         43.50 ns  lf=0.596  
find_empty                          43.11 ns  lf=0.000  

insert                             483.95 ns  lf=0.596    314.7 MB
insert_predicted                   329.77 ns  lf=0.596    314.7 MB
insert_replace                     261.35 ns  lf=0.596    314.7 MB

emplace                            480.89 ns  lf=0.596    314.7 MB
emplace_predicted                  328.18 ns  lf=0.596    314.7 MB
emplace_replace                    270.50 ns  lf=0.596    314.7 MB

try_emplace                        488.02 ns  lf=0.596    314.7 MB
try_emplace_predicted              331.29 ns  lf=0.596    314.7 MB
try_emplace_replace                413.96 ns  lf=0.596    314.7 MB

operator []                        485.09 ns  lf=0.596    314.7 MB
operator [] predicted              344.99 ns  lf=0.596    314.7 MB
operator [] replace                208.28 ns  lf=0.596    314.7 MB

serial_erase                       236.05 ns  lf=0.000    314.7 MB
random_erase                       411.94 ns  lf=0.000    314.7 MB
erase_failed                        46.44 ns  lf=0.596    314.7 MB

toggle                             121.60 ns  lf=0.000    314.7 MB
iterate                             19.96 ns  lf=0.596    314.7 MB

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

serial_find_success                121.49 ns  lf=0.596  
random_find_success                167.45 ns  lf=0.596  
find_failed                         43.98 ns  lf=0.596  
find_empty                           3.41 ns  lf=0.000  

insert                             430.19 ns  lf=0.596    297.3 MB (+39.5 MB)
insert_predicted                   344.44 ns  lf=0.596    297.3 MB (+47.5 MB)
insert_replace                     229.54 ns  lf=0.596    344.9 MB

emplace                            424.84 ns  lf=0.596    305.3 MB (+39.5 MB)
emplace_predicted                  347.58 ns  lf=0.596    305.3 MB (+39.5 MB)
emplace_replace                    236.64 ns  lf=0.596    344.9 MB

try_emplace                        445.21 ns  lf=0.596    305.3 MB (+39.5 MB)
try_emplace_predicted              349.59 ns  lf=0.596    305.3 MB (+39.5 MB)
try_emplace_replace                345.72 ns  lf=0.596    344.9 MB

operator []                        429.03 ns  lf=0.596    305.3 MB (+39.5 MB)
operator [] predicted              344.46 ns  lf=0.596    305.3 MB (+39.5 MB)
operator [] replace                144.35 ns  lf=0.596    344.9 MB

serial_erase                       253.51 ns  lf=0.000    344.9 MB
random_erase                       436.58 ns  lf=0.000    344.9 MB
erase_failed                        49.34 ns  lf=0.596    344.9 MB

toggle                             139.90 ns  lf=0.000    305.3 MB
iterate                              4.20 ns  lf=0.596    344.9 MB
ktprime commented 2 years ago

最新测试结果 https://martin.ankerl.com/2022/08/27/hashmap-bench-01/

shines77 commented 2 years ago

看到了, 先回复一下, 我记得前几天也有两个回复, 你删掉了还是编辑过?

ktprime commented 2 years ago

看到了, 先回复一下, 我记得前几天也有两个回复, 你删掉了还是编辑过?

可能编辑过吧,他的展示图做的真好 尽管emhash7/8/dense 在他那里测试综合排名很好,实际场景我认为一般, 我的项目基本只用emhash5(没参与测试,查询性能应该是目前最好的)

ktprime commented 2 years ago

期待你的robin + simd 优化,至少技术上看起来是很完美,就看最终实现效果 作者说5年内不再做测试了

shines77 commented 2 years ago

其实 jstd::robin_hash_map 前几天已经改好了, 但是测 string 的速度不是很理想, 前几天看到了想回复一下的, 一直没回上.

昨天和今天, 我改了一下, 速度终于好了一点, 基本跟 emhash8 差不多了, 我想起来了前几天你的关于 absl::flat_hash_map 的回复.

我待会再回复你, 先给家人打个电话, 中秋节.

shines77 commented 2 years ago

原来你是在 https://github.com/shines77/jstd_hashmap/issues/1 里问的,我新建了一个 issue,统一在 https://github.com/shines77/hashmap-benchmark/issues/4 里回复了。

shines77 commented 2 years ago

期待你的robin + simd 优化,至少技术上看起来是很完美,就看最终实现效果 作者说5年内不再做测试了

目前 jstd::robin_hash_map 里没怎么用 simd,虽然我写了 simd 代码,就只在 rehash() 里用了 group.matchUsed()。因为 simd 起不到关键性的作用,只能改善哈希冲突的长度比较大的时候的性能,但实际使用环境大多数时候冲突的长度都不会特别大,所以提升的比例也不是很大,只能说有的话,就更健壮一点。而且处理在大对象的 metadata 的时候,可能使用 simd 效果更差,所以目前我一直没怎么管 simd 。

shines77 commented 2 years ago

最近测试我改进的swiss table- emilib2/3. 我的实现查询性能比官方慢20%,插入则快20%,一直分析对比查找哪里查询慢(我在乎查询性能),一个区别在于我使用线性探测,而swiss table使用二次探测,还有你之前说的swiss做了随机内存地址优化,这些难道是主要原因?

结合这个提问,我说一下最近遇到的问题。

我在给 jstd::robin_hash_map 添加处理大对象的能力,也就是使用间接性 key(使用 index 指向 slots),一开始,我就想把 absl::flat_hashmap 那个 ctrls 和 slots_ 内存一起分配的方法给实现了,因为理论上它应该有提升。结果,我花了半天才实现的(因为跟 absl::flat_hash_map 有点不一样,我不想仅仅只对齐到 alignas(slot_type),我想对齐到CacheLineSize,只能重写分配和对齐的算法),结果测试发现不进没快反而慢了不少。我没有立刻改回去,我的想法是等所有间接性 key 的代码完成之后,发现还是没有改善,再改回去试试。

好了,前两天,间接性 key 的代码终于改完并测试通过了,发现确实速度不太行,昨天和今天,我就把 ctrls 和 slots 统一分配改回分开分配了,发现速度就正常了。但我也保留了统一分配的代码,可以使用 #define ROBIN_USE_SWAP_TRAITS 0 启用统一分配。

其中是什么道理,我也没想明白了。本来统一分配的优点是,少了一次内存分配,ctrls 和 slots 是连续的,能减少一点点 TLB miss 的几率(虽然只有一点点提升,但也是提升啊),缺点是,多了处理偏移值和对齐的代码,但理论上应该能抵消一次内存分配。

但事实上,我看到的原因可能都不是这些,从测试程序显示的内存占用可以看到,使用统一分配时,内存占用是 ctrls + slots 的总内存,而使用分开单独分配 ctrls 和 slots 的内存时,内存的占用只统计了 slots 的内存使用量,ctrls 的内存是不计入其中的。

这在测试 ./bin/time_hash_map_new string 时可以看到,如果使用统一分配,处理 625000 个 <std::string, std::string> 时,内存使用量是 53.9~54.1 MB 之间,而使用单独分配,处理 625000 个 <std::string, std::string> 时,内存使用量是 37.9 MB,这个内存使用量跟 emhash8 和 ankerl::unordered_dense::map 都是一样的,说明 emhash8 和 ankerl::unordered_dense::map 同样也是采用 ctrls(metadata) 和 slots(entries) 分开分配的。

我计算了一下,625000 个 metadata(8字节=4字节dist+hash,4字节index),也只需要 625000 * 8 = 4.768 MB,不知道为何使用统一分配,内存使用量反而多了 20MB,多出来了 15MB 。我没有检查过 sizeof(metadata) 的大小,但是应该是 8 字节吧。

从结果上来看,就是使用单独分配的话,ctrls 的内存看起来不是在堆上分配的,从这个现象看起来,有点像是在栈上分配的,所以才导致了实际性能更快?但栈上应该是不能分配 4.768 MB这么大的内存吧,所以可能是分配在别的地方,反正不是在堆上,所以统计内存使用量时,未计算在内,但是为何会更快,就不得而知了。

如果这是正确的,那么 absl::flat_hash_map 如果也改成单独分配 ctrls 和 slots,性能应该有提升,但是这可能跟内存分配器有关。但是默认的内存分配器中,单独分配性能会更好。

shines77 commented 2 years ago

hashmap-benchmark 中我更新了你的 emhash 和我的 jstd_hashmap 到最新版。

另外,我更新了 time_hash_map 的测试代码,之前我加入了 is_transparent 的支持,但为了公平起见,因为有的 hash_map 不支持哈希函数的 is_transparent,我在大对象的 HashObject 上,取消了 is_transparent 的支持。但小对象上是支持的,不过小对象上使用 is_transparent 基本没有意义,所以目前 is_transparent 基本等于没有(之前是大小对象都支持 is_transparent 的)。

ktprime commented 2 years ago

emhahs8最早是统一分配内存。不久前改成独立分配,内存减少一点,主要原因 是rehash 的时候可以先删除metadata再重新分配减少峰值内存。

数据少可以统一分配,否则分开分配,我想试试改进emilib2测试一下

shines77 commented 2 years ago

目前 jstd::robin_hash_map 在大对象的处理上,对于 std::string,速度跟 emhash8 比较接近,慢于 ankerl::unordered_dense::map,稍慢于 robin_hood::unordered_map。对于 HashObejct<uint64_t, 256, 64>,慢于 ankerl::unordered_dense::map 和 emhash8,无法测试 robin_hood::unordered_map,跟 absl::flat_hash_map 相当,emhash8 比 ankerl::unordered_dense::map 还稍稍快一点。

我才发现我的 time_hash_map 里没有加入 emhash8 的测试,已添加。

另外,现在还可以让 Martin Leitner-Ankerl 把我的 jstd::robin_hash_map 加入 https://martin.ankerl.com/2022/08/27/hashmap-bench-01/ 的测试吗?