指针的最快散列函数是什么
What is the fastest hash function for pointers?
基于哈希表的容器是非常快速的关联数组(例如unordered_map
、unordered_set
)。
它们的性能在很大程度上取决于用于为每个条目创建索引的哈希函数。随着哈希表的增长,元素会被一次又一次地重新哈希。
指针是简单的类型,基本上是一个4/8字节的值,用于唯一标识对象。问题是,由于几个LSB为零,因此使用地址作为哈希函数的结果是无效的。
示例:
struct MyVoidPointerHash {
size_t operator()(const void* val) const {
return (size_t)val;
}
};
更快的实现是丢失一些位:
struct MyVoidPointerHash2 {
size_t operator()(const void* val) const {
return ((size_t)val) >> 3; // 3 on 64 bit, 1 on 32 bit
}
};
后者使大型应用程序的性能提高了10-20%,该应用程序使用哈希集和映射,其中包含数万个频繁构建和清除的元素。
有人能提供一个更好的哈希指针方案吗?
功能需要是:
- 快!并且必须很好地内联
- 提供合理的分布,允许罕见的碰撞
更新-基准结果
我运行了两组测试,一组用于int*
,另一组用于大小为4KB的类指针。结果非常有趣。
我使用std::unordered_set
进行所有测试,数据大小为16MB,在单个new
调用中分配。第一种算法运行了两次,以确保缓存尽可能热,并且CPU全速运行。
安装程序:VS2013(x64)、i7-2600、Windows 8.1 x64。
- VS2013默认哈希函数
- 哈希1:
return (size_t)(val);
- 哈希2:
return '(size_t)(val) >> 3;
- Hash3(@BasileStarynkevitch):
uintptr_t ad = (uintptr_t)val; return (size_t)((13 * ad) ^ (ad >> 15));
- 哈希4(@Roddy):
uintptr_t ad = (uintptr_t)val; return (size_t)(ad ^ (ad >> 16));
- 哈希5(@egur):
代码:
template<typename Tval>
struct MyTemplatePointerHash1 {
size_t operator()(const Tval* val) const {
static const size_t shift = (size_t)log2(1 + sizeof(Tval));
return (size_t)(val) >> shift;
}
};
测试1-int*
:
- VS2013默认耗时1292ms
- Hash1耗时742ms
- Hash2耗时343ms
- Hash3耗时1008ms
- Hash4耗时629ms
- Hash5耗时350ms
测试1-4K_class*
:
- VS2013默认耗时0.423ms
- Hash1耗时23.889ms
- Hash2耗时6.331毫秒
- Hash3耗时0.366ms
- Hash4耗时0.390ms
- Hash5耗时0.290ms
更新2:
到目前为止,模板化哈希(Hash5)函数是赢家。针对各种块大小的速度提供最佳性能水平。
更新3:添加了基线的默认哈希函数。事实证明,这远远不是最佳的。
从理论角度来看,正确的答案是:使用std::hash
,它可能会尽可能地专业化,如果不适用,请使用好的哈希函数,而不是快速的哈希函数。散列函数的速度与其说是质量,不如说是质量。
实用的答案是:使用std::hash
,它很差,但性能却出奇地好。
在产生兴趣之后,我在周末进行了大约30个小时的基准测试。除其他外,我试图得到一个平均情况与最坏情况的比较,并试图通过故意在插入的集合大小方面对bucket计数给出不好的提示,迫使std::unordered_map
进入最坏情况。
我将较差的哈希(std::hash<T*>
)与众所周知的总体质量良好的通用哈希(djb2,sdbm)以及这些哈希的变体进行了比较,这些哈希解释了非常短的输入长度,并与明确认为在哈希表中使用的哈希(murmur2和murmur3)进行了比较
由于对齐,指针上最低的2-3位总是零,我认为测试一个简单的右移是值得的;hash";,因此在哈希表例如仅使用最低的N个比特的情况下仅使用非零信息。事实证明,对于合理的移位(我也尝试过不合理的移位!)这实际上表现得很好。
调查结果
我的一些发现是众所周知的,并不令人惊讶,其他发现则非常令人惊讶:
- 很难预测什么是";"好";搞砸编写好的散列函数很难。不足为奇,众所周知的事实,并再次证明
- 没有任何一个哈希在任何情况下都能显著优于所有其他哈希。在80%的时间里,没有一个哈希能显著优于所有其他哈希。第一个结果是意料之中的,但第二个结果却令人惊讶
- 要迫使
std::unordered_map
表现得不好真的很难。即使对bucket计数进行了故意的糟糕提示,这将迫使进行多次重新散列,总体性能也不会差多少。只有最糟糕的哈希函数以近乎荒谬的方式丢弃了大部分熵,才能显著影响性能10-20%以上(比如right_shift_12
,它实际上只会为50000个输入产生12个不同的哈希值!在这种情况下,哈希图的运行速度慢了大约100倍并不奇怪——我们基本上是在链表上进行随机访问查找。) - 一些";有趣的";结果肯定要归功于实现细节。我的实现(GCC)使用略大于2^N的素数桶计数,并将带有缩进散列的值插入链表中
std::hash<T*>
的专业化对于GCC(一个简单的reinterpret_cast
)来说是非常可悲的。有趣的是,做相同事情的函子在插入时始终表现得更快,在随机访问时表现得更慢。差异很小(8-10秒的测试运行需要十几毫秒),但它不是噪声,而是持续存在的——可能与指令重新排序或流水线有关。令人震惊的是,完全相同的代码(这也是一个非操作)在两种不同的场景中始终表现得不同- 可悲的散列并不执行明显比";"好";哈希或为哈希表显式设计的哈希。事实上,有一半的时候,他们是表现最好的,或者说是前三名
- ";最好的";散列函数很少(如果有的话)产生最佳的整体性能
- 在这个SO问题中,作为答案发布的哈希通常还可以。它们是好的平均值,但并不优于
std::hash
。通常他们会进入前3-4名 - 较差的散列在某种程度上容易受到插入顺序的影响(在随机插入和随机插入之后的随机查找上执行得更差);"好";散列对插入顺序的影响更有弹性(几乎没有差异),但总体性能仍然稍慢
测试设置
测试不仅是任何4字节或8字节(或任何)对齐的值,而且是通过在堆上分配完整的元素集并将分配器提供的地址存储在std::vector
中获得的实际地址(然后删除对象,不需要它们)
按照存储在矢量中的顺序,将地址插入到新分配的std::unordered_map
中,一次按原始顺序("顺序"),一次在矢量上应用std::random_shuffle
之后。
对大小分别为4、16、64、256和1024的50000和1000000个对象集进行了测试(为简洁起见,此处省略了64个对象的结果,它们位于16和256之间的中间位置——StackOverflow只允许发布30k个字符)
测试套件执行了3次,结果不时变化3或4毫秒,但总体相同。这里公布的结果是最后一次跑步。
在";"随机";测试以及访问模式(在每个测试中)都是伪随机的,但对于测试运行中的每个哈希函数来说都是完全相同的。
哈希基准测试下的时间用于在一个整数变量中汇总400000000个哈希值。
列insert
是创建std::unordered_map
、分别插入50000和1000000个元素以及销毁映射的50次迭代的时间(以毫秒为单位)。
列access
是对"向量"中的伪随机元素进行100000000次查找,然后在unordered_map
中查找该地址的时间(以毫秒为单位)
这个时间包括平均一个用于访问vector
中的随机元素的缓存丢失,至少对于大数据集(小数据集完全适合L2)。
2.66GHz Intel Core 2、Windows 7、gcc 4.8.1/MinGW-w64_32上的所有计时。计时器粒度@1ms。
源代码
源代码在Ideone上可用,同样是因为Stackoverflow的30k字符限制。
注意:在台式电脑上运行完整的测试套件需要2个多小时,因此,如果您想重现结果,请准备好散散步。
测试结果
Benchmarking hash funcs...
std::hash 2576
reinterpret_cast 2561
djb2 13970
djb2_mod 13969
sdbm 10246
yet_another_lc 13966
murmur2 11373
murmur3 15129
simple_xorshift 7829
double_xorshift 13567
right_shift_2 5806
right_shift_3 5866
right_shift_4 5705
right_shift_5 5691
right_shift_8 5795
right_shift_12 5728
MyTemplatePointerHash1 5652
BasileStarynkevitch 4315
--------------------------------
sizeof(T) = 4
sizeof(T*) = 4
insertion order = sequential
dataset size = 50000 elements
name insert access
std::hash 421 6988
reinterpret_cast 408 7083
djb2 451 8875
djb2_mod 450 8815
sdbm 455 8673
yet_another_lc 443 8292
murmur2 478 9006
murmur3 490 9213
simple_xorshift 460 8591
double_xorshift 477 8839
right_shift_2 416 7144
right_shift_3 422 7145
right_shift_4 414 6811
right_shift_5 425 8006
right_shift_8 540 11787
right_shift_12 1501 49604
MyTemplatePointerHash1 410 7138
BasileStarynkevitch 445 8014
--------------------------------
sizeof(T) = 4
sizeof(T*) = 4
insertion order = random
dataset size = 50000 elements
name insert access
std::hash 443 7570
reinterpret_cast 436 7658
djb2 473 8791
djb2_mod 472 8766
sdbm 472 8817
yet_another_lc 458 8419
murmur2 479 9005
murmur3 491 9205
simple_xorshift 464 8591
double_xorshift 476 8821
right_shift_2 441 7724
right_shift_3 440 7716
right_shift_4 450 8061
right_shift_5 463 8653
right_shift_8 649 16320
right_shift_12 3052 114185
MyTemplatePointerHash1 438 7718
BasileStarynkevitch 453 8140
--------------------------------
sizeof(T) = 4
sizeof(T*) = 4
insertion order = sequential
dataset size = 1000000 elements
name insert access
std::hash 8945 32801
reinterpret_cast 8796 33251
djb2 11139 54855
djb2_mod 11041 54831
sdbm 11459 36849
yet_another_lc 14258 57350
murmur2 16300 39024
murmur3 16572 39221
simple_xorshift 14930 38509
double_xorshift 16192 38762
right_shift_2 8843 33325
right_shift_3 8791 32979
right_shift_4 8818 32510
right_shift_5 8775 30436
right_shift_8 10505 35960
right_shift_12 30481 91350
MyTemplatePointerHash1 8800 33287
BasileStarynkevitch 12885 37829
--------------------------------
sizeof(T) = 4
sizeof(T*) = 4
insertion order = random
dataset size = 1000000 elements
name insert access
std::hash 12183 33424
reinterpret_cast 12125 34000
djb2 22693 51255
djb2_mod 22722 51266
sdbm 15160 37221
yet_another_lc 24125 51850
murmur2 16273 39020
murmur3 16587 39270
simple_xorshift 16031 38628
double_xorshift 16233 38757
right_shift_2 11181 33896
right_shift_3 10785 33660
right_shift_4 10615 33204
right_shift_5 10357 38216
right_shift_8 15445 100348
right_shift_12 73773 1044919
MyTemplatePointerHash1 11091 33883
BasileStarynkevitch 15701 38092
--------------------------------
sizeof(T) = 64
sizeof(T*) = 4
insertion order = sequential
dataset size = 50000 elements
name insert access
std::hash 415 8243
reinterpret_cast 422 8321
djb2 445 8730
djb2_mod 449 8696
sdbm 460 9439
yet_another_lc 455 9003
murmur2 475 9109
murmur3 482 9313
simple_xorshift 463 8694
double_xorshift 465 8900
right_shift_2 416 8402
right_shift_3 418 8405
right_shift_4 423 8366
right_shift_5 421 8347
right_shift_8 453 9195
right_shift_12 666 18008
MyTemplatePointerHash1 433 8191
BasileStarynkevitch 466 8443
--------------------------------
sizeof(T) = 64
sizeof(T*) = 4
insertion order = random
dataset size = 50000 elements
name insert access
std::hash 450 8135
reinterpret_cast 457 8208
djb2 470 8736
djb2_mod 476 8698
sdbm 483 9420
yet_another_lc 476 8953
murmur2 481 9089
murmur3 486 9283
simple_xorshift 466 8663
double_xorshift 468 8865
right_shift_2 456 8301
right_shift_3 456 8302
right_shift_4 453 8337
right_shift_5 457 8340
right_shift_8 505 10379
right_shift_12 1099 34923
MyTemplatePointerHash1 464 8226
BasileStarynkevitch 466 8372
--------------------------------
sizeof(T) = 64
sizeof(T*) = 4
insertion order = sequential
dataset size = 1000000 elements
name insert access
std::hash 9548 35362
reinterpret_cast 9635 35869
djb2 10668 37339
djb2_mod 10763 37311
sdbm 11126 37145
yet_another_lc 11597 39944
murmur2 16296 39029
murmur3 16432 39280
simple_xorshift 16066 38645
double_xorshift 16108 38778
right_shift_2 8966 35953
right_shift_3 8916 35949
right_shift_4 8973 35504
right_shift_5 8941 34997
right_shift_8 9356 31233
right_shift_12 13831 45799
MyTemplatePointerHash1 8839 31798
BasileStarynkevitch 15349 38223
--------------------------------
sizeof(T) = 64
sizeof(T*) = 4
insertion order = random
dataset size = 1000000 elements
name insert access
std::hash 14756 36237
reinterpret_cast 14763 36918
djb2 15406 38771
djb2_mod 15551 38765
sdbm 14886 37078
yet_another_lc 15700 40290
murmur2 16309 39024
murmur3 16432 39381
simple_xorshift 16177 38625
double_xorshift 16073 38750
right_shift_2 14732 36961
right_shift_3 14170 36965
right_shift_4 13687 37295
right_shift_5 11978 35135
right_shift_8 11498 46930
right_shift_12 25845 268052
MyTemplatePointerHash1 10150 32046
BasileStarynkevitch 15981 38143
--------------------------------
sizeof(T) = 256
sizeof(T*) = 4
insertion order = sequential
dataset size = 50000 elements
name insert access
std::hash 432 7957
reinterpret_cast 429 8036
djb2 462 8970
djb2_mod 453 8884
sdbm 460 9110
yet_another_lc 466 9015
murmur2 495 9147
murmur3 494 9300
simple_xorshift 479 8792
double_xorshift 477 8948
right_shift_2 430 8120
right_shift_3 429 8132
right_shift_4 432 8196
right_shift_5 437 8324
right_shift_8 425 8050
right_shift_12 519 11291
MyTemplatePointerHash1 425 8069
BasileStarynkevitch 468 8496
--------------------------------
sizeof(T) = 256
sizeof(T*) = 4
insertion order = random
dataset size = 50000 elements
name insert access
std::hash 462 7956
reinterpret_cast 456 8046
djb2 490 9002
djb2_mod 483 8905
sdbm 482 9116
yet_another_lc 492 8982
murmur2 492 9120
murmur3 492 9276
simple_xorshift 477 8761
double_xorshift 477 8903
right_shift_2 458 8116
right_shift_3 459 8124
right_shift_4 462 8281
right_shift_5 463 8370
right_shift_8 458 8069
right_shift_12 662 16244
MyTemplatePointerHash1 459 8091
BasileStarynkevitch 472 8476
--------------------------------
sizeof(T) = 256
sizeof(T*) = 4
insertion order = sequential
dataset size = 1000000 elements
name insert access
std::hash 9756 34368
reinterpret_cast 9718 34897
djb2 10935 36894
djb2_mod 10820 36788
sdbm 11084 37857
yet_another_lc 11125 37996
murmur2 16522 39078
murmur3 16461 39314
simple_xorshift 15982 38722
double_xorshift 16151 38868
right_shift_2 9611 34997
right_shift_3 9571 35006
right_shift_4 9135 34750
right_shift_5 8978 32878
right_shift_8 8688 30276
right_shift_12 10591 35827
MyTemplatePointerHash1 8721 30265
BasileStarynkevitch 15524 38315
--------------------------------
sizeof(T) = 256
sizeof(T*) = 4
insertion order = random
dataset size = 1000000 elements
name insert access
std::hash 14169 36078
reinterpret_cast 14096 36637
djb2 15373 37492
djb2_mod 15279 37438
sdbm 15531 38247
yet_another_lc 15924 38779
murmur2 16524 39109
murmur3 16422 39280
simple_xorshift 16119 38735
double_xorshift 16136 38875
right_shift_2 14319 36692
right_shift_3 14311 36776
right_shift_4 13932 35682
right_shift_5 12736 34530
right_shift_8 9221 30663
right_shift_12 15506 98465
MyTemplatePointerHash1 9268 30697
BasileStarynkevitch 15952 38349
--------------------------------
sizeof(T) = 1024
sizeof(T*) = 4
insertion order = sequential
dataset size = 50000 elements
name insert access
std::hash 421 7863
reinterpret_cast 419 7953
djb2 457 8983
djb2_mod 455 8927
sdbm 445 8609
yet_another_lc 446 8892
murmur2 492 9090
murmur3 507 9294
simple_xorshift 467 8687
double_xorshift 472 8872
right_shift_2 432 8009
right_shift_3 432 8014
right_shift_4 435 7998
right_shift_5 442 8099
right_shift_8 432 7914
right_shift_12 462 8911
MyTemplatePointerHash1 426 7744
BasileStarynkevitch 467 8417
--------------------------------
sizeof(T) = 1024
sizeof(T*) = 4
insertion order = random
dataset size = 50000 elements
name insert access
std::hash 452 7948
reinterpret_cast 456 8018
djb2 489 9037
djb2_mod 490 8992
sdbm 477 8795
yet_another_lc 491 9179
murmur2 502 9078
murmur3 507 9273
simple_xorshift 473 8671
double_xorshift 480 8873
right_shift_2 470 8105
right_shift_3 470 8100
right_shift_4 476 8333
right_shift_5 468 8065
right_shift_8 469 8094
right_shift_12 524 10216
MyTemplatePointerHash1 451 7826
BasileStarynkevitch 472 8419
--------------------------------
sizeof(T) = 1024
sizeof(T*) = 4
insertion order = sequential
dataset size = 1000000 elements
name insert access
std::hash 10910 38432
reinterpret_cast 10892 38994
djb2 10499 38985
djb2_mod 10507 38983
sdbm 11318 37450
yet_another_lc 11740 38260
murmur2 16960 39544
murmur3 16816 39647
simple_xorshift 16096 39021
double_xorshift 16419 39183
right_shift_2 10219 38909
right_shift_3 10012 39036
right_shift_4 10642 40284
right_shift_5 10116 38678
right_shift_8 9083 32914
right_shift_12 9376 31586
MyTemplatePointerHash1 8777 30129
BasileStarynkevitch 16036 38913
--------------------------------
sizeof(T) = 1024
sizeof(T*) = 4
insertion order = random
dataset size = 1000000 elements
name insert access
std::hash 16304 38695
reinterpret_cast 16241 39641
djb2 16377 39533
djb2_mod 16428 39526
sdbm 15402 37811
yet_another_lc 16180 38730
murmur2 16679 39355
murmur3 16792 39586
simple_xorshift 16196 38965
double_xorshift 16371 39127
right_shift_2 16445 39263
right_shift_3 16598 39421
right_shift_4 16378 39839
right_shift_5 15517 38442
right_shift_8 11589 33547
right_shift_12 11992 49041
MyTemplatePointerHash1 9052 30222
BasileStarynkevitch 16163 38829
让这个问题解决一段时间后,我将发布迄今为止最好的指针哈希函数:
template<typename Tval>
struct MyTemplatePointerHash1 {
size_t operator()(const Tval* val) const {
static const size_t shift = (size_t)log2(1 + sizeof(Tval));
return (size_t)(val) >> shift;
}
};
它对各种块大小都具有高性能
如果有人有更好的功能,我会更改已接受的答案。
散列函数返回的结果的类型为size_t
,但容器会将其转换为"bucket索引",从而确定定位对象的正确bucket。
我认为标准中没有指定这种转换:但我认为这通常是一个Modulo N运算,其中N是存储桶的数量,而N通常是2的幂,因为当命中次数太多时,将存储桶计数加倍是增加大小的好方法。Modulo N运算意味着,对于指针,朴素的散列函数只使用一小部分桶。
真正的问题是,容器的"好"哈希算法必须基于对bucket大小和哈希值的了解。例如,如果存储在表中的对象的大小都是1024字节,那么每个指针的低位10位可能是相同的。
struct MyOneKStruct x[100]; //bottom 10 bits of &x[n] are always the same
因此,任何应用程序的"最佳"哈希都可能需要大量的尝试、错误和测量,以及对哈希值分布的了解。
然而,与其简单地将指针向下移动N位,我还不如尝试将顶部的"单词"异或到底部的单词中。很像@BasileStarynkevich的回答。
关于添加哈希表的建议读起来很有趣。我在以下段落中强调:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1456.html
不可能编写一个完全通用的有效哈希函数适用于所有类型。(不能只将对象转换为原始内存对字节进行散列;除其他原因外,这个想法之所以失败,是因为填充。)正因为如此,也因为一个好的哈希函数是只有在特定使用模式的上下文中才有效,它是必不可少的以允许用户提供他们自己的散列函数。
显然,答案取决于系统和处理器(特别是因为页面大小和单词大小)。我提议
struct MyVoidPointerHash {
size_t operator()(const void* val) const {
uintptr_t ad = (uintptr_t) val;
return (size_t) ((13*ad) ^ (ad >> 15));
}
};
深入了解到,在许多系统上,页面大小通常为4K字节(即212),因此右移>>15
将把有效地址部分放在低位。13*
主要是为了好玩(但13是素数),并对更多比特进行混洗。exclusive或^
正在混合比特,并且速度非常快。因此,散列的低位是指针的许多位(高位和低位)的混合。
我并没有声称在这样的散列函数中投入了很多"科学"。但它们往往运行得很好。YMMV。我想你应该避免停用ASLR
在性能赛道上无法击败您的解决方案(无论是char
还是1024大小的struct
),但在正确性方面有一些改进:
#include <iostream>
#include <new>
#include <algorithm>
#include <unordered_set>
#include <chrono>
#include <cstdlib>
#include <cstdint>
#include <cstddef>
#include <cmath>
namespace
{
template< std::size_t argument, std::size_t base = 2, bool = (argument < base) >
constexpr std::size_t log = 1 + log< argument / base, base >;
template< std::size_t argument, std::size_t base >
constexpr std::size_t log< argument, base, true > = 0;
}
struct pointer_hash
{
template< typename type >
constexpr
std::size_t
operator () (type * p) const noexcept
{
return static_cast< std::size_t >(reinterpret_cast< std::uintptr_t >(p) >> log< std::max(sizeof(type), alignof(type)) >);
}
};
template< typename type = std::max_align_t, std::size_t i = 0 >
struct alignas(alignof(type) << i) S
{
};
int
main()
{
constexpr std::size_t _16M = (1 << 24);
S<> * p = new S<>[_16M]{};
auto latch = std::chrono::high_resolution_clock::now();
{
std::unordered_set< S<> *, pointer_hash > s;
for (auto * pp = p; pp < p + _16M; ++pp) {
s.insert(pp);
}
}
std::cout << std::chrono::duration_cast< std::chrono::milliseconds >(std::chrono::high_resolution_clock::now() - latch).count() << "ms" << std::endl;
delete [] p;
return EXIT_SUCCESS;
}
- 在两台机器之间进行时间戳的最佳c++chrono函数是什么
- 具有相同特征的两个对象是否只在内存中存储一次?无论定义它们的函数是什么,都是不同的
- 这里的字符串函数是什么意思
- 这个函数是什么意思(我的英语sry)
- C++ 中的 use 函数是什么?
- C++中的编译时函数是什么?
- 使用 DnsQuery 或 getaddrinfo 的正确函数是什么?
- Lua 中看起来像表的函数是什么?
- "AfxIsValidAddress"函数的等效标准函数是什么?
- 子类的构造函数后跟冒号后的基类构造函数是什么意思?
- MFC 用于计算控件的高光、阴影等的算法或函数是什么?
- 为非标准对象定义散列函数和相等函数
- 对于自定义类的一个未定义集,是否有一个默认的散列函数
- 具有多个非可选参数的转换构造函数是什么样子的?为什么它有意义
- 不确定 c++ 中的常量函数是什么
- 字典中使用了什么散列函数(hash_table)
- 指针的最快散列函数是什么
- 获得快速的k对独立散列函数的选项是什么
- 当我在Unorder容器中使用位集时,散列函数是什么样子的
- 这个简单的散列函数背后的逻辑是什么