PHP内核探索之PHP中的哈希表

在PHP内核中,其中一个很重要的数据结构就是HashTable。我们常用的数组,在内核中就是用HashTable来实现。那么,PHP的HashTable是怎么实现的呢?最近在看HashTable的数据结构,但是算法书籍里面没有具体的实现算法,刚好最近也在阅读PHP的源码,于是参考PHP的HashTable的实现,自己实现了一个简易版的HashTable,总结了一些心得,下面给大家分享一下。

笔者github上有一个简易版的HashTable的实现:HashTable实现

另外,我在github有对PHP源码更详细的注解。感兴趣的可以围观一下,给个star。PHP5.4源码注解。可以通过commit记录查看已添加的注解。

HashTable的介绍

哈希表是实现字典操作的一种有效数据结构。

定义

简单地说,HashTable(哈希表)就是一种键值对的数据结构。支持插入,查找,删除等操作。在一些合理的假设下,在哈希表中的所有操作的时间复杂度是O(1)(对相关证明感兴趣的可以自行查阅)。

实现哈希表的关键

在哈希表中,不是使用关键字做下标,而是通过哈希函数计算出key的哈希值作为下标,然后查找/删除时再计算出key的哈希值,从而快速定位元素保存的位置。

在一个哈希表中,不同的关键字可能会计算得到相同的哈希值,这叫做“哈希冲突”,就是处理两个或多个键的哈希值相同的情况。解决哈希冲突的方法有很多,开放寻址法,拉链法等等。

因此,实现一个好的哈希表的关键就是一个好的哈希函数和处理哈希冲突的方法。

Hash函数

判断一个哈希算法的好坏有以下四个定义: > * 一致性,等价的键必然产生相等的哈希值; > * 高效性,计算简便; > * 均匀性,均匀地对所有的键进行哈希。

哈希函数建立了关键值与哈希值的对应关系,即:h = hash_func(key)。对应关系见下图:

设计一个完美的哈希函数就交由专家去做吧,我们只管用已有的较成熟的哈希函数就好了。PHP内核使用的哈希函数是time33函数,又叫DJBX33A,其实现如下:


  1. static inline ulong zend_inline_hash_func(const char *arKey, uint nKeyLength) 
  2.          register ulong hash = 5381; 
  3.  
  4.         /* variant with the hash unrolled eight times */ 
  5.         for (; nKeyLength >= 8; nKeyLength -= 8) { 
  6.             hash = ((hash << 5) + hash) + *arKey++; 
  7.             hash = ((hash << 5) + hash) + *arKey++; 
  8.             hash = ((hash << 5) + hash) + *arKey++; 
  9.             hash = ((hash << 5) + hash) + *arKey++; 
  10.             hash = ((hash << 5) + hash) + *arKey++; 
  11.             hash = ((hash << 5) + hash) + *arKey++; 
  12.             hash = ((hash << 5) + hash) + *arKey++; 
  13.             hash = ((hash << 5) + hash) + *arKey++; 
  14.     } 
  15.  
  16.     switch (nKeyLength) { 
  17.         case 7: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ 
  18.         case 6: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ 
  19.         case 5: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ 
  20.         case 4: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ 
  21.         case 3: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ 
  22.         case 2: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ 
  23.         case 1: hash = ((hash << 5) + hash) + *arKey++; break; 
  24.         case 0: break; 
  25.         EMPTY_SWITCH_DEFAULT_CASE() 
  26.     } 
  27.     return hash; 

注:函数使用了一个8次循环+switch来实现,是对for循环的优化,减少循环的运行次数,然后在switch里面执行剩下的没有遍历到的元素。

拉链法

将所有具有相同哈希值的元素都保存在一条链表中的方法叫拉链法。查找的时候通过先计算key对应的哈希值,然后根据哈希值找到对应的链表,最后沿着链表顺序查找相应的值。 具体保存后的结构图如下:

PHP的HashTable结构

简单地介绍了哈希表的数据结构之后,继续看看PHP中是如何实现哈希表的。


  1. typedef struct _hashtable { 
  2.           uint nTableSize; 
  3.           uint nTableMask; 
  4.           uint nNumOfElements; 
  5.           ulong nNextFreeElement; 
  6.           Bucket *pInternalPointer; 
  7.           Bucket *pListHead; 
  8.           Bucket *pListTail;  
  9.           Bucket **arBuckets; 
  10.           dtor_func_t pDestructor; 
  11.           zend_bool persistent; 
  12.           unsigned char nApplyCount; 
  13.           zend_bool bApplyProtection; 
  14.           #if ZEND_DEBUG 
  15.                int inconsistent; 
  16.           #endif 
  17. } HashTable; 
  • nTableSize,HashTable的大小,以2的倍数增长
  • nTableMask,用在与哈希值做与运算获得该哈希值的索引取值,arBuckets初始化后永远是nTableSize-1
  • nNumOfElements,HashTable当前拥有的元素个数,count函数直接返回这个值
  • nNextFreeElement,表示数字键值数组中下一个数字索引的位置
  • pInternalPointer,内部指针,指向当前成员,用于遍历元素
  • pListHead,指向HashTable的第一个元素,也是数组的第一个元素
  • pListTail,指向HashTable的最后一个元素,也是数组的最后一个元素。与上面的指针结合,在遍历数组时非常方便,比如reset和endAPI
  • arBuckets,包含bucket组成的双向链表的数组,索引用key的哈希值和nTableMask做与运算生成
  • pDestructor,删除哈希表中的元素使用的析构函数
  • persistent,标识内存分配函数,如果是TRUE,则使用操作系统本身的内存分配函数,否则使用PHP的内存分配函数
  • nApplyCount,保存当前bucket被递归访问的次数,防止多次递归
  • bApplyProtection,标识哈希表是否要使用递归保护,默认是1,要使用

举一个哈希与mask结合的例子:

例如,”foo”真正的哈希值(使用DJBX33A哈希函数)是193491849。如果我们现在有64容量的哈希表,我们明显不能使用它作为数组的下标。取而代之的是通过应用哈希表的mask,然后只取哈希表的低位。

hash           |        193491849  |     0b1011100010000111001110001001
& mask         | &             63  | &   0b0000000000000000000000111111
----------------------------------------------------------------------
= index        | = 9               | =   0b0000000000000000000000001001

因此,在哈希表中,foo是保存在arBuckets中下标为9的bucket向量中。

bucket结构体的定义


  1. typedef struct bucket { 
  2.      ulong h; 
  3.      uint nKeyLength; 
  4.      void *pData; 
  5.      void *pDataPtr; 
  6.      struct bucket *pListNext; 
  7.      struct bucket *pListLast; 
  8.      struct bucket *pNext; 
  9.      struct bucket *pLast; 
  10.      const char *arKey; 
  11. } Bucket; 
  • h,哈希值(或数字键值的key
  • nKeyLength,key的长度
  • pData,指向数据的指针
  • pDataPtr,指针数据
  • pListNext,指向HashTable中的arBuckets链表中的下一个元素
  • pListLast,指向HashTable中的arBuckets链表中的上一个元素
  • pNext,指向具有相同hash值的bucket链表中的下一个元素
  • pLast,指向具有相同hash值的bucket链表中的上一个元素
  • arKey,key的名称

PHP中的HashTable是采用了向量加双向链表的实现方式,向量在arBuckets变量保存,向量包含多个bucket的指针,每个指针指向由多个bucket组成的双向链表,新元素的加入使用前插法,即新元素总是在bucket的第一个位置。由上面可以看到,PHP的哈希表实现相当复杂。这是它使用超灵活的数组类型要付出的代价。

一个PHP中的HashTable的示例图如下所示:

HashTable相关API

  • zend_hash_init
  • zend_hash_add_or_update
  • zend_hash_find
  • zend_hash_del_key_or_index

zend_hash_init

函数执行步骤

  • 设置哈希表大小
  • 设置结构体其他成员变量的初始值 (包括释放内存用的析构函数pDescructor)

详细代码注解点击:zend_hash_init源码

注:

1、pHashFunction在此处并没有用到,php的哈希函数使用的是内部的zend_inline_hash_func

2、zend_hash_init执行之后并没有真正地为arBuckets分配内存和计算出nTableMask的大小,真正分配内存和计算nTableMask是在插入元素时进行CHECK_INIT检查初始化时进行。

zend_hash_add_or_update

函数执行步骤

  • 检查键的长度
  • 检查初始化
  • 计算哈希值和下标
  • 遍历哈希值所在的bucket,如果找到相同的key且值需要更新,则更新数据,否则继续指向bucket的下一个元素,直到指向bucket的最后一个位置
  • 为新加入的元素分配bucket,设置新的bucket的属性值,然后添加到哈希表中
  • 如果哈希表空间满了,则重新调整哈希表的大小

函数执行流程图

CONNECT_TO_BUCKET_DLLIST是将新元素添加到具有相同hash值的bucket链表。

CONNECT_TO_GLOBAL_DLLIST是将新元素添加到HashTable的双向链表。

详细代码和注解请点击:zend_hash_add_or_update代码注解

zend_hash_find

函数执行步骤

  • 计算哈希值和下标
  • 遍历哈希值所在的bucket,如果找到key所在的bucket,则返回值,否则,指向下一个bucket,直到指向bucket链表中的最后一个位置

详细代码和注解请点击:zend_hash_find代码注解

zend_hash_del_key_or_index

函数执行步骤

  • 计算key的哈希值和下标
  • 遍历哈希值所在的bucket,如果找到key所在的bucket,则进行第三步,否则,指向下一个bucket,直到指向bucket链表中的最后一个位置
  • 如果要删除的是第一个元素,直接将arBucket[nIndex]指向第二个元素;其余的操作是将当前指针的last的next执行当前的next
  • 调整相关指针
  • 释放数据内存和bucket结构体内存

详细代码和注解请点击:zend_hash_del_key_or_index代码注解

性能分析

PHP的哈希表的优点:PHP的HashTable为数组的操作提供了很大的方便,无论是数组的创建和新增元素或删除元素等操作,哈希表都提供了很好的性能,但其不足在数据量大的时候比较明显,从时间复杂度和空间复杂度看看其不足。

不足如下:

  • 保存数据的结构体zval需要单独分配内存,需要管理这个额外的内存,每个zval占用了16bytes的内存;
  • 在新增bucket时,bucket也是额外分配,也需要16bytes的内存;
  • 为了能进行顺序遍历,使用双向链表连接整个HashTable,多出了很多的指针,每个指针也要16bytes的内存;
  • 在遍历时,如果元素位于bucket链表的尾部,也需要遍历完整个bucket链表才能找到所要查找的值

PHP的HashTable的不足主要是其双向链表多出的指针及zval和bucket需要额外分配内存,因此导致占用了很多内存空间及查找时多出了不少时间的消耗。

后续

上面提到的不足,在PHP7中都很好地解决了,PHP7对内核中的数据结构做了一个大改造,使得PHP的效率高了很多,因此,推荐PHP开发者都将开发和部署版本更新吧。看看下面这段PHP代码:


  1. <?php 
  2. $size = pow(2, 16);  
  3.  
  4. $startTime = microtime(true); 
  5. $array = array(); 
  6. for ($key = 0, $maxKey = ($size - 1) * $size; $key <= $maxKey; $key += $size) { 
  7.     $array[$key] = 0; 
  8. $endTime = microtime(true); 
  9. echo '插入 ', $size, ' 个恶意的元素需要 ', $endTime - $startTime, ' 秒', "\n"; 
  10.  
  11. $startTime = microtime(true); 
  12. $array = array(); 
  13. for ($key = 0, $maxKey = $size - 1; $key <= $maxKey; ++$key) { 
  14.     $array[$key] = 0; 
  15. $endTime = microtime(true); 
  16. echo '插入 ', $size, ' 个普通元素需要 ', $endTime - $startTime, ' 秒', "\n"; 

上面这个demo是有多个hash冲突时和无冲突时的时间消耗比较。笔者在PHP5.4下运行这段代码,结果如下

插入 65536 个恶意的元素需要 43.72204709053 秒

插入 65536 个普通元素需要 0.009843111038208 秒

而在PHP7上运行的结果:

插入 65536 个恶意的元素需要 4.4028408527374 秒

插入 65536 个普通元素需要 0.0018510818481445 秒

可见不论在有冲突和无冲突的数组操作,PHP7的性能都提升了不少,当然,有冲突的性能提升更为明显。至于为什么PHP7的性能提高了这么多,值得继续深究。

作者:风满楼

来源:51CTO

时间: 2024-08-01 08:26:14

PHP内核探索之PHP中的哈希表的相关文章

php内核解析:PHP中的哈希表_php技巧

PHP中使用最为频繁的数据类型非字符串和数组莫属,PHP比较容易上手也得益于非常灵活的数组类型. 在开始详细介绍这些数据类型之前有必要介绍一下哈希表(HashTable). 哈希表是PHP实现中尤为关键的数据结构. 哈希表在实践中使用的非常广泛,例如编译器通常会维护的一个符号表来保存标记,很多高级语言中也显式的支持哈希表. 哈希表通常提供查找(Search),插入(Insert),删除(Delete)等操作,这些操作在最坏的情况下和链表的性能一样为O(n). 不过通常并不会这么坏,合理设计的哈希

[PHP内核探索]PHP中的哈希表

在PHP内核中,其中一个很重要的数据结构就是HashTable.我们常用的数组,在内核中就是用HashTable来实现.那么,PHP的HashTable是怎么实现的呢?最近在看HashTable的数据结构,但是算法书籍里面没有具体的实现算法,刚好最近也在阅读PHP的源码,于是参考PHP的HashTable的实现,自己实现了一个简易版的HashTable,总结了一些心得,下面给大家分享一下. 笔者github上有一个简易版的HashTable的实现:HashTable实现 另外,我在github有

在C#中应用哈希表

      一,哈希表(Hashtable)简述      在.NET Framework中,Hashtable是System.Collections命名空间提供的一个容器,用于处理和表现类似key/value的键值对,其中key通常可用来快速查找,同时key是区分大小写:value用于存储对应于key的值.Hashtable中key/value键值对均为object类型,所以Hashtable可以支持任何类型的key/value键值对.        二,哈希表的简单操作       在哈希表

PHP内核探索:哈希表碰撞攻击原理_php实例

下面通过图文并茂的方式给大家展示PHP内核探索:哈希表碰撞攻击原理. 最近哈希表碰撞攻击(Hashtable collisions as DOS attack)的话题不断被提起,各种语言纷纷中招.本文结合PHP内核源码,聊一聊这种攻击的原理及实现.  哈希表碰撞攻击的基本原理 哈希表是一种查找效率极高的数据结构,很多语言都在内部实现了哈希表.PHP中的哈希表是一种极为重要的数据结构,不但用于表示Array数据类型,还在Zend虚拟机内部用于存储上下文环境信息(执行上下文的变量及函数均使用哈希表结

PHP内核探索之变量- 不平凡的字符串

切,一个字符串有什么好研究的.   别这么说,看过<平凡的世界>么,平凡的字符串也可以有不平凡的故事.试看:   (1)       在C语言中,strlen计算字符串的时间复杂度是?PHP中呢?   (2)       在PHP中,怎样处理多字节字符串?PHP对unicode的支持如何?   同样是字符串,为什么c语言与C++/PHP/Java的均不相同?   数据结构决定算法,这句话一点不假.   那么我们今天就来掰一掰,PHP中的字符串结构,以及相关字符串函数的实现.   一.  字符串

十天学Linux内核之第一天---内核探索工具类

原文:十天学Linux内核之第一天---内核探索工具类 寒假闲下来了,可以尽情的做自己喜欢的事情,专心待在实验室里燥起来了,因为大二的时候接触过Linux,只是关于内核方面确实是不好懂,所以十天的时间里还是希望能够补充一下Linux内核相关知识,接下来继续待在实验室里想总结一下Linux内核编程,十天肯定完全掌握不了Linux内核,这里我也只是把自己认为不是很好懂并且很重要的难点疑点写出来,和大家一起分享,希望大家改正互相学习. Linux的具体概述这里就不多说了,今天主要讲的是Linux内核中

PHP内核探索之变量(3)- hash table

原文:PHP内核探索之变量(3)- hash table        在PHP中,除了zval, 另一个比较重要的数据结构非hash table莫属,例如我们最常见的数组,在底层便是hash table.除了数组,在线程安全(TSRM).GC.资源管理.Global变量.ini配置管理中,几乎都有Hash table的踪迹(上一次我们也提到,符号表也是使用Hash table实现的).那么,在PHP中,这种数据有什么特殊之处,结构是怎么实现的? 带着这些问题,我们开始本次的内核探索之旅.   

PHP内核探索之变量(4)- 数组操作

原文:PHP内核探索之变量(4)- 数组操作 上一节(PHP内核探索之变量(3)- hash table),我们已经知道,数组在PHP的底层实际上是HashTable(链接法解决冲突),本文将对最常用的函数系列-数组操作的相关函数做进一步的跟踪. 本文主要内容: PHP中提供的数组操作函数 数组操作函数的实现 结语参考文献 一.PHP中提供的数组操作函数 可以说,数组是PHP中使用最广泛的数据结构之一,正因如此,PHP为开发者提供了丰富的数组操作函数(参见http://cn2.php.net/m

PHP内核探索之变量(5)- session的基本原理

原文:PHP内核探索之变量(5)- session的基本原理 这次说说session. session可以说是当前互联网提到的最多的名词之一了.它的含义很宽泛,可以指任何一次完整的事务交互(会话):如发送一次HTTP请求并接受响应,执行一条SQL语句都可以看做一次Session.如无特殊说明,本文中提到的Session单指HTTP会话. 本文是PHP内核探索的第五篇,主要包含如下几个方面的内容: 背景知识和session基础 PHP中session的原理 参考文献 一.背景知识,session基