指针的最快散列函数是什么

What is the fastest hash function for pointers?

本文关键字:散列函数 是什么 指针      更新时间:2023-10-16

基于哈希表的容器是非常快速的关联数组(例如unordered_mapunordered_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%,该应用程序使用哈希集和映射,其中包含数万个频繁构建和清除的元素。

有人能提供一个更好的哈希指针方案吗?

功能需要是:

  1. 快!并且必须很好地内联
  2. 提供合理的分布,允许罕见的碰撞

更新-基准结果

我运行了两组测试,一组用于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;
}