PHP哈希表结构的深入剖析

分享于:2017-07-01 20:53:55

1.png


1355372439_5252.jpg

我们知道在C语言里数组是一个基本的内存块(chunk of memory),所以使用一定要明确数组长度而动态数组几乎是不可能的,同理associative array(关联数组)这种形式的也是不存在的,但在PHP里面数组是一个很灵活的数据结构,当然不仅仅PHP在现代动态语言的实现中几乎都存在这种动态灵活的数据结构,比如JS, python等等,那么他们是如何实现的,这就要用到一个结构哈希表。很多动态语言的核心其实就是一张哈希表。


哈希表最关键的几个方面有:

  1. 通过key访问(key的确定,哈希函数)

  2.  映射到数据结构中(哈希表本身的存储结构)

  3. 映射的处理(冲突或者碰撞检测和处理函数)


对于PHP的哈希我们也从上面三个方面进行分析。

理解PHP的哈希算法

一般来说对于整形索引进行哈希我们很容易想到的是取模运算,比如array(1=>'a', 2=>'b', 3=>'c'),这类我们可以使用index%3来哈希,不过PHP数组的下标还有更灵活的array('a'='c', 'b'=>'d'),此时选择什么哈希函数?答案是DJBX33A算法。

PS:DJBX33A算法,也就是time33算法(学院里有专门条目介绍了),是APR默认哈希算法,php, apache, perl, bsddb也都使用time33哈希。对于33这个数,DJB注释中是说,1到256之间的所有奇数,都能达到一个可接受的哈希分布,平均分布大概是86%。而其中33,17,31,63,127,129这几个数在面对大量的哈希运算时有一个更大的优势,就是这些数字能将乘法用位运算配合加减法替换,这样运算速度会更高。gcc编译器开启优化后会自动将乘法转换为位运算。

下面就是这个哈希函数的具体代码实现:

1.png

理解HashTable的结构定义

有了哈希函数之后那么哈希表本身的存储结构如何?这里需要说明两种PHP底层的数据结构HashTable 和 Bucket

1.png

上述结构体定义了PHP底层的存储结构,逐个字段做个解释:

nNumOfElements。是PHP数组中实际存储元素的个数,我们使用count,sizeof计算的就是获取的这个值。

nTableSize。顾名思义这个是整个哈希表分配的大小(在内部实现的C中分配的数组大小,PHP是动态的但到底层数组是有大小的是静态的),他的大小有一个固定的申请算法,一般是最接近并且大于当前这个数值的2的乘方,描述的可能有点模糊,举个例子来看,如果PHP数组存储32个整形数据,那么底层申请的nTableSize应该等于32个元素,如果33呢,那么取最近且大于这个数的一个数64,那么分配的大小是64个元素。这样分配的原因是为了能分配足够的内存同样又不会浪费太多的内存。基于哈希的效率考虑,太小那么势必造成哈希之后太多的碰撞查找,如果分配太大那么必然浪费太多内存,这样分配经过实践证明相对在空间和时间上可以获得一个平衡。

nTableMask。哈希表的掩码数值等于nTableSize-1,他的作用是什么?用来纠正通过上面DBJ算法计算的哈希值在当前nTableSize大小的哈希表中的正确的索引值。比如"foo"通过固定算法之后得出的哈希值是193491849,如果表的大小为64,很明显已经超过了最大索引值,这时候就需要运用哈希表的掩码对其进行矫正实际采用的方法就是与掩码进行位运与运算,这样做是为了把哈希值大的一样映射到nTalbeSize空间内。

1.png

nNextFreeElement。下一个空闲的元素空间,当我们申请一个空下标元素的时候就需要用到此项,比如$ret[] = 'apple'。

pInternalPointer。存储了内部当前执行的元素的指针,当我们使用一些内部循环函数的时候会用到这个指针比如reset(), current(), prev(), next(), foreach(), end()。

pListHead和pListTail则具体指向了该哈希表的第一个和最后一个元素,对应就是数组的起始和结束元素。

arBuckets。这个就是实际存储的C的内部数组,具体的结构后面还会详细讨论。这里记录的是一个指向指针的指针Bucket **。

pDestructor 是一个析构函数,当某个值被从哈希表删除的时候会触发此函数。他还有一个主要作用是用于变量的GC回收。在PHP里面GC是通过引用计数实现的,当一个变量的引用计数变为0,就会被PHP的GC回收。

persistent 定义了hashtable是否能在多次request中获得持久存在。

nApplyCount 和 bApplyProtection 是用来防止无限递归的。

inconsistent 是在调试模式下捕获对HT不正确的使用。

1.png

h是一个哈希值,未经过掩码矫正的哈希DBJ算出来的原始值。

arKey,用来记录作为哈希计算的字符串,nKeyLength是哈希字符串的长度,对于整形键值是用不到这两项的。

pData以及pDataPtr是实际存储数据的指针,在PHP里面他们通常是指向一个zval结构(该结构广泛被PHP用来内部存储各种变量以及对象)。

pListNext, pListLast 指定了整个数组的顺序,PHP中的遍历就是通过哈希结构体中的pListHead bucket依次遍历pNext直到数组结束。

pNext和pLast 这两个指针是用来解决哈希冲突的,这个在下面哈希冲突中详细介绍,在PHP的哈希表冲突的处理采用的是拉链法也就是在每个可能冲突的键值位置拉出一个链表来存储对应的键值数据(哈希冲突还有什么解决方法?寻址法不过在PHP中并是通过这个方式实现的)

哈希冲突的处理

关于哈希冲突,PHP的实现是通过拉链法实现的,当键值被哈希到同一个槽位(bucket)就是发生了冲突,这时候会从bucket拉出一个链表把冲突的元素顺序链接起来。pListNext,pListLast就是实现这个拉链的结构的。

至此PHP的哈希的基本结构介绍完毕,实现是非常complex的,但对比灵活无比的PHP数组这点点复杂性值,太值得了。

关于那两队对指针,国外有网站上搞错了,这里把检测哈希冲突的PHP函数贴出来,pNext指针的作用就一目了然了。

1.png

来源:http://www.nowamagic.net/academy/detail/1201011