[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,其实现如下:
static inline ulong zend_inline_hash_func(const char *arKey, uint nKeyLength)
{
register ulong hash = 5381;
/* variant with the hash unrolled eight times */
for (; nKeyLength >= 8; nKeyLength -= 8) {
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
hash = ((hash << 5) + hash) + *arKey++;
}
switch (nKeyLength) {
case 7: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
case 6: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
case 5: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
case 4: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
case 3: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
case 2: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
case 1: hash = ((hash << 5) + hash) + *arKey++; break;
case 0: break;
EMPTY_SWITCH_DEFAULT_CASE()
}
return hash;
}
注:函数使用了一个8次循环+switch来实现,是对for循环的优化,减少循环的运行次数,然后在switch里面执行剩下的没有遍历到的元素。
拉链法
将所有具有相同哈希值的元素都保存在一条链表中的方法叫拉链法。查找的时候通过先计算key对应的哈希值,然后根据哈希值找到对应的链表,最后沿着链表顺序查找相应的值。
具体保存后的结构图如下:
PHP的HashTable结构
简单地介绍了哈希表的数据结构之后,继续看看PHP中是如何实现哈希表的。
PHP内核hashtable的定义:
typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements;
ulong nNextFreeElement;
Bucket *pInternalPointer;
Bucket *pListHead;
Bucket *pListTail;
Bucket **arBuckets;
dtor_func_t pDestructor;
zend_bool persistent;
unsigned char nApplyCount;
zend_bool bApplyProtection;
#if ZEND_DEBUG
int inconsistent;
#endif
} 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结构体的定义
typedef struct bucket {
ulong h;
uint nKeyLength;
void *pData;
void *pDataPtr;
struct bucket *pListNext;
struct bucket *pListLast;
struct bucket *pNext;
struct bucket *pLast;
const char *arKey;
} 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代码:
<?php
$size = pow(2, 16);
$startTime = microtime(true);
$array = array();
for ($key = 0, $maxKey = ($size - 1) * $size; $key <= $maxKey; $key += $size) {
$array[$key] = 0;
}
$endTime = microtime(true);
echo '插入 ', $size, ' 个恶意的元素需要 ', $endTime - $startTime, ' 秒', "\n";
$startTime = microtime(true);
$array = array();
for ($key = 0, $maxKey = $size - 1; $key <= $maxKey; ++$key) {
$array[$key] = 0;
}
$endTime = microtime(true);
echo '插入 ', $size, ' 个普通元素需要 ', $endTime - $startTime, ' 秒', "\n";
上面这个demo是有多个hash冲突时和无冲突时的时间消耗比较。笔者在PHP5.4下运行这段代码,结果如下
插入 65536 个恶意的元素需要 43.72204709053 秒
插入 65536 个普通元素需要 0.009843111038208 秒
而在PHP7上运行的结果:
插入 65536 个恶意的元素需要 4.4028408527374 秒
插入 65536 个普通元素需要 0.0018510818481445 秒
可见不论在有冲突和无冲突的数组操作,PHP7的性能都提升了不少,当然,有冲突的性能提升更为明显。至于为什么PHP7的性能提高了这么多,值得继续深究。
最后再安利一下,笔者github上有一个简易版的HashTable的实现:HashTable实现
另外,我在github有对PHP源码更详细的注解。感兴趣的可以围观一下,给个star。PHP5.4源码注解。可以通过commit记录查看已添加的注解。
原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。
如果本文对你有帮助,请点下推荐吧,谢谢^_^
参考文章:
Understanding PHP's internal array implementation (PHP's Source Code for PHP Developers - Part 4)
PHP's new hashtable implementation
[PHP内核探索]PHP中的哈希表的更多相关文章
- Delphi 中的哈希表: THashedStringList
转自万一博客 Delphi 中的哈希表: THashedStringList unit Unit1; interface uses Windows, Messages, SysUtils, Var ...
- Python 中的哈希表
Python 中的哈希表:对字典的理解 有没有想过,Python中的字典为什么这么高效稳定.原因是他是建立在hash表上.了解Python中的hash表有助于更好的理解Python,因为Pytho ...
- OVS 中的哈希表: shash
shash出现在OVS的代码中,定义如下: struct hmap_node { size_t hash; struct hmap_node * next; }; struct ...
- Freemarker 中的哈希表(Map)和序列(List)
freemarlker中的容器类型有: 哈希表:是实现了TemplateHashModel或者TemplateHashModelEx接口的java对象,经常使用的实现类是SimpleHash,该类实现 ...
- [转]:Delphi 中的哈希表(1): THashedStringList
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms ...
- Delphi 中的哈希表(二)—— TStringHash
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms ...
- PHP内核探索:哈希碰撞攻击是什么?
最近哈希表碰撞攻击(Hashtable collisions as DOS attack)的话题不断被提起,各种语言纷纷中招.本文结合PHP内核源码,聊一聊这种攻击的原理及实现. 哈希表碰撞攻击的基本 ...
- php内核探索 [转]
PHP内核探索:从SAPI接口开始 PHP内核探索:一次请求的开始与结束 PHP内核探索:一次请求生命周期 PHP内核探索:单进程SAPI生命周期 PHP内核探索:多进程/线程的SAPI生命周期 PH ...
- PHP服务器脚本 PHP内核探索:新垃圾回收机制说明
在5.2及更早版本的PHP中,没有专门的垃圾回收器GC(Garbage Collection),引擎在判断一个变量空间是否能够被释放的时候是依据这个变量的zval的refcount的值,如果refco ...
随机推荐
- 旺财速啃H5框架之Bootstrap(四)
上一篇<<旺财速啃H5框架之Bootstrap(三)>>已经把导航做了,接下来搭建内容框架.... 对于不规整的网页,要做成自适应就有点玩大了.... 例如下面这种版式的页面. ...
- socket读写返回值的处理
在调用socket读写函数read(),write()时,都会有返回值.如果没有正确处理返回值,就可能引入一些问题 总结了以下几点 1当read()或者write()函数返回值大于0时,表示实际从缓冲 ...
- 3.Windows Server 2012 R2数据库部署
很多人竟然不会安装数据库....好吧,来个图文教程,其实和windows里面一样安装,和安装2008一样的 先安装3.5:http://www.cnblogs.com/dunitian/p/53487 ...
- ASP.NET MVC开发:Web项目开发必备知识点
最近加班加点完成一个Web项目,使用Asp.net MVC开发.很久以前接触的Asp.net开发还是Aspx形式,什么Razor引擎,什么MVC还是这次开发才明白,可以算是新手. 对新手而言,那进行A ...
- Dropzone.js实现文件拖拽上传
dropzone.js是一个开源的JavaScript库,提供 AJAX 异步文件上传功能,支持拖拽文件.支持最大文件大小.支持设置文件类型.支持预览上传结果,不依赖jQuery库. 使用Dropzo ...
- 使用JavaScript为一张图片设置备选路径
在做网页开发的时候,有时候希望给图片设置一个备选路径,即,当src属性对应的主路径加载失败的时候,图片可以马上切换到备选路径.这样,即使主路径失效了,显示备用路径也不会影响网页的正常体验. 注意到网页 ...
- webix前端架构的项目应用
webix框架兼容javascript.HTML.CSS,应用比较灵活,应用框架时,配合后台webAPI,整个web项目里面,App文件夹保存前台的多语言文件,图片文件,webix原代码js.css, ...
- 三星Note 7停产,原来是吃了流程的亏
三星Note 7发售两个月即成为全球噩梦,从首炸到传言停产仅仅47天.所谓"屋漏偏逢连天雨",相比华为.小米等品牌对其全球市场的挤压.侵蚀,Galaxy Note 7爆炸事件这场连 ...
- 记录在Windows上安装和使用Oracle数据库过程中的坑
1.安装Oracle Oracle软件是免费的,可以去官网下载相应的安装包.但是如果用于商业用途需要购买License.官网上针对各种平台,32位和64位都有,如果在Windows一般会下载到两个文件 ...
- Centos 7.0 安装Mono 3.4 和 Jexus 5.6
2013-07-26 写过一篇<CentOS 6.3下 安装 Mono 3.2 和Jexus 5.4>,CentOS 7在CentOS 6的基础上有很大的调整,本文是这篇文章的更新,主要介 ...