PHP哈希表结构的深入剖析

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

  • 前面提到了PHP里的哈希算法,还有哈希表的数据结构实现,还有Times33算法等,但是我觉得我到现在还没有完全理解啊。能不能再细致点、深入点地去理解呢?
  • 那这样吧,配合下面的图来理解一下前面条目里的哈希表的数据结构定义吧。有图就好理解多了。这个图基本反应了PHP hashtable的一个基本结构示例。
点击查看大图

我们知道在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编译器开启优化后会自动将乘法转换为位运算。

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

01 static inline ulong zend_inline_hash_func(char *arKey, uint nKeyLength){      
02     register ulong hash = 5381;     /* variant with the hash unrolled eight times */   
03     for (; nKeyLength >= 8; nKeyLength -= 8) {       
04         hash = ((hash << 5) + hash) + *arKey++;       
05         hash = ((hash << 5) + hash) + *arKey++;       
06         hash = ((hash << 5) + hash) + *arKey++;       
07         hash = ((hash << 5) + hash) + *arKey++;       
08         hash = ((hash << 5) + hash) + *arKey++;       
09         hash = ((hash << 5) + hash) + *arKey++;       
10         hash = ((hash << 5) + hash) + *arKey++;       
11         hash = ((hash << 5) + hash) + *arKey++;   
12     }
13     switch (nKeyLength) {       
14         case 7: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */       
15         case 6: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */       
16         case 5: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */       
17         case 4: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */       
18         case 3: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */       
19         case 2: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */       
20         case 1: hash = ((hash << 5) + hash) + *arKey++; break;       
21         case 0: break;
22         EMPTY_SWITCH_DEFAULT_CASE()   
23     }
24     return hash;
25 }
理解HashTable的结构定义

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

01 typedef struct _hashtable {
02     uint nTableSize;        // hash Bucket的大小,最小为8,以2x增长。
03     uint nTableMask;        // nTableSize-1 , 索引取值的优化
04     uint nNumOfElements;    // hash Bucket中当前存在的元素个数,count()函数会直接返回此值
05     ulong nNextFreeElement; // 下一个数字索引的位置
06     Bucket *pInternalPointer;   // 当前遍历的指针(foreach比for快的原因之一)
07     Bucket *pListHead;          // 存储数组头元素指针
08     Bucket *pListTail;          // 存储数组尾元素指针
09     Bucket **arBuckets;         // 存储hash数组
10     dtor_func_t pDestructor;
11     zend_bool persistent;
12     unsigned char nApplyCount; // 标记当前hash Bucket被递归访问的次数(防止多次递归)
13     zend_bool bApplyProtection;// 标记当前hash桶允许不允许多次访问,不允许时,最多只能递归3次
14 #if ZEND_DEBUG
15     int inconsistent;
16 #endif
17 } HashTable;

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

  1. nNumOfElements。是PHP数组中实际存储元素的个数,我们使用count,sizeof计算的就是获取的这个值。
  2. nTableSize。顾名思义这个是整个哈希表分配的大小(在内部实现的C中分配的数组大小,PHP是动态的但到底层数组是有大小的是静态的),他的大小有一个固定的申请算法,一般是最接近并且大于当前这个数值的2的乘方,描述的可能有点模糊,举个例子来看,如果PHP数组存储32个整形数据,那么底层申请的nTableSize应该等于32个元素,如果33呢,那么取最近且大于这个数的一个数64,那么分配的大小是64个元素。这样分配的原因是为了能分配足够的内存同样又不会浪费太多的内存。基于哈希的效率考虑,太小那么势必造成哈希之后太多的碰撞查找,如果分配太大那么必然浪费太多内存,这样分配经过实践证明相对在空间和时间上可以获得一个平衡。
  3. nTableMask。哈希表的掩码数值等于nTableSize-1,他的作用是什么?用来纠正通过上面DBJ算法计算的哈希值在当前nTableSize大小的哈希表中的正确的索引值。比如"foo"通过固定算法之后得出的哈希值是193491849,如果表的大小为64,很明显已经超过了最大索引值,这时候就需要运用哈希表的掩码对其进行矫正实际采用的方法就是与掩码进行位运与运算,这样做是为了把哈希值大的一样映射到nTalbeSize空间内。
    1    hash  |   193491849 |   0b1011100010000111001110001001
    2  & mask  | &        63 | & 0b0000000000000000000000111111
    3 ---------------------------------------------------------
    4  = index | =         9 | = 0b0000000000000000000000001001
  4. nNextFreeElement。下一个空闲的元素空间,当我们申请一个空下标元素的时候就需要用到此项,比如$ret[] = 'apple'。
  5. pInternalPointer。存储了内部当前执行的元素的指针,当我们使用一些内部循环函数的时候会用到这个指针比如reset(), current(), prev(), next(), foreach(), end()。
  6. pListHead和pListTail则具体指向了该哈希表的第一个和最后一个元素,对应就是数组的起始和结束元素。
  7. arBuckets。这个就是实际存储的C的内部数组,具体的结构后面还会详细讨论。这里记录的是一个指向指针的指针Bucket **。
  8. pDestructor 是一个析构函数,当某个值被从哈希表删除的时候会触发此函数。他还有一个主要作用是用于变量的GC回收。在PHP里面GC是通过引用计数实现的,当一个变量的引用计数变为0,就会被PHP的GC回收。
  9. persistent 定义了hashtable是否能在多次request中获得持久存在。
  10. nApplyCount 和 bApplyProtection 是用来防止无限递归的。
  11. inconsistent 是在调试模式下捕获对HT不正确的使用。
  • 经这么一说,我对上一个条目中哈希表的数据结构定义清晰好多了。还有Buckets桶呢,Buckets结构是Hash table中真正的数据存储单元,我们来讨论下吧。
理解bucket的结构定义
01 typedef struct bucket {
02     ulong h;
03     uint nKeyLength;
04     void *pData;
05     void *pDataPtr;
06     struct bucket *pListNext;
07     struct bucket *pListLast;
08     struct bucket *pNext;
09     struct bucket *pLast;
10     const char *arKey;
11 } Bucket;
  1. h是一个哈希值,未经过掩码矫正的哈希DBJ算出来的原始值。
  2. arKey,用来记录作为哈希计算的字符串,nKeyLength是哈希字符串的长度,对于整形键值是用不到这两项的。
  3. pData以及pDataPtr是实际存储数据的指针,在PHP里面他们通常是指向一个zval结构(该结构广泛被PHP用来内部存储各种变量以及对象)。
  4. pListNext, pListLast 指定了整个数组的顺序,PHP中的遍历就是通过哈希结构体中的pListHead bucket依次遍历pNext直到数组结束。
  5. pNext和pLast 这两个指针是用来解决哈希冲突的,这个在下面哈希冲突中详细介绍,在PHP的哈希表冲突的处理采用的是拉链法也就是在每个可能冲突的键值位置拉出一个链表来存储对应的键值数据(哈希冲突还有什么解决方法?寻址法不过在PHP中并是通过这个方式实现的)
哈希冲突的处理

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

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

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

01 ZEND_API int zend_hash_exists(const HashTable *ht, const char *arKey, uint nKeyLength)
02 {
03     ulong h;
04     uint nIndex;
05     Bucket *p;
06  
07     IS_CONSISTENT(ht);
08  
09     h = zend_inline_hash_func(arKey, nKeyLength);
10     nIndex = h & ht->nTableMask;
11  
12     p = ht->arBuckets[nIndex];
13     while (p != NULL) {
14         if (p->arKey == arKey ||
15             ((p->h == h) && (p->nKeyLength == nKeyLength) && !memcmp(p->arKey, arKey, nKeyLength))) {
16                 return 1;
17         }
18         p = p->pNext;
19     }
20     return 0;
21 }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值