哈希游戏- 哈希游戏平台- 哈希游戏官方网站下文的图5.1展示了TR1中unordered_map的内存和访问模式。让我们来看看当我们访问和键名“Johannesburg”相关的GPS坐标的时候会发生什么。这个键名被哈希并映射到了bucket #0。在那我们跳到了此bucket的链表的第一个节点(bucket #0左边的橙色箭头),我们可以访问堆中存储了键“Johannesburg”所属数据的内存区域(节点右侧的黑色箭头)。如果键名所指向的第一个节点不可用,就必须遍历其他的节点来访问。
的实例,其中Key和T分别是键名和键值的模版参数。在64位架构下储存字符串的时候,pair的实例大小是16字节。下文的图5.2是dense_hash_map内存和访问模式的展示。如果我们要寻找“Johannesburg”的坐标,我们一开始会进入bucket #0,其中有“Paris”(译注:图上实际应为“Dubai”)的数据(bucket #0右侧的黑色箭头)。因此必须探测然后跳转到bucket (i + 1) = (0 + 1) = 1(bucket #0左侧的橙色箭头),然后就能在bucket #1中找到“Johannesburg”的数据。这看上去和unordered_map中做的事情差不多,但其实完全不同。当然,和unordered_map一样,键名和键值都必须存储在分配于堆中的内存,这将导致对键名和键值的寻找会使缓存行无效化。但为碰撞的条目寻找一个bucket相对较快一些。实际上既然每个pair都是16字节而大多数处理器上的缓存行都是64字节,每次探测就像是在同一个缓存行上。这将急剧提高运算速度,与之相反的是unordered_map中的链表需要在RAM中跳转以寻找余下的节点。
的内存哈希表,但其是设计用于在硬盘上持久化的。哈希表的元数据和用户数据一起用文件系统依次存储在硬盘上唯一的文件中。 Kyoto Cabinet使用每个bucket中独立的二叉树处理碰撞。Bucket数组长度固定且不改变大小,无视负载因子的状态。这是Kyoto Cabinet的哈希表实现的主要缺陷。实际上,如果数据库创建的时候定义的bucket数组的长度低于实际需求,当条目开始碰撞的时候性能会急剧下降。允许硬盘上的哈希表实现改变bucket数组大小是很难的。首先,其需要bucket数组和条目存储到两个不同的文件中,其大小会各自独立的增长。第二,因为调整bucket数组大小需要将键名重新哈希到新bucket数组的新位置,这需要从硬盘中读取所有条目的键名,这对于相当大的数据库来说代价太高以至于几乎不可能。避免这种重新哈希过程的一种方法是,存储哈希后键名的时候每个条目预留4或8个字节(取决于哈希是长度32还是64 bit)。因为这些麻烦事,固定长度的bucket数组更简单,而Kyoto Cabinet中采用了这个方法。
把数据条目和节点一起存储在单一记录中,乍一看像是设计失误,但其实是相当聪明的。为了存储一个条目的数据,总是需要保持三种不同的数据:bucket、碰撞和条目。既然bucket数组中的bucket必须顺序存储,其需要就这样存储并且没有任何该进的方法。假设我们保存的不是整型而是不能存储在bucket中的字符或可变长度字节数组,这使其必须访问此bucket数组区域之外的其他内存。这样当添加一个新条目的时候,需要即保存冲突数据结构的数据,又要保存该条目键名和键值的数据。
如果冲突和条目数据分开保存,其需要访问硬盘两次,再加上必须的对bucket的访问。如果要设置新值,其需要总计3次写入,并且写入的位置可能相差很远。这表示是在硬盘上的随机写入,这差不多是I/O的最糟糕的情况了。现在既然Kyoto Cabinet的HashDB中节点数据和条目数据存储在一起,其就可以只用一次写入写到硬盘中。当然,仍然必须访问bucket,但如果bucket数组足够小,就可以通过操作系统将其从硬盘中缓存到RAM中。如规范中Effective Implementation of Hash Database一节声明的,Kyoto Cabinet可能采用这种方式。
本文中,我展示了三个不同哈希表库的数据组织和内存访问模式。TR1的unordered_map和SparseHash的dense_hash_map是在内存中的,而Kyoto Cabinet的HashDB是在硬盘上的。此三者用不同的访问处理碰撞,并对性能有不同的英雄。将bucket数据、碰撞数据和条目数据各自分开将影响性能,这是unordered_map中出现的情况。如dense_hash_map及其二次内部探测那样,将碰撞数据和bucket存储在一起;或者像HashDB那样,将碰撞数据和条目数据存储在一起都可以大幅提高速度。此两者都可以提高写入速度,但将碰撞数据和bucket存储在一起可以使读取更快。