当前位置: 首页 > news >正文

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

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

http://www.jsqmd.com/news/1106166/

相关文章:

  • 系列09-Playwright UI 自动化平台怎么设计?MQ 调度与 Runner 执行架构
  • 前后端分离考研互助交流平台系统|SpringBoot+Vue+MyBatis+MySQL完整源码+部署教程
  • Go escape逃逸分析
  • AI文生图技术解析与商业应用实战指南
  • 网络变压器行业的全球前十强品牌主要分为国际头部厂商和国产领先企业两大阵营。
  • 深信服SangFor 8.0.95版本防火墙配置
  • Codex 用了一个月,SSD 少了 4.8TB——AI 编程工具暗藏的 5 个资源陷阱与终极方案
  • 孤能子视角:Karpathy LLM Wiki,一个人工观察符自动编织系统
  • 被需要是一种高级自由,这种被需要感能激发出你最深层的智慧和韧性。
  • 全书目录与章节地图 《AI Agent 开发平台资深技术专家 AI Agent 应用架构师 CTO 面试题库详解》
  • 第4章 RAG 检索增强生成全链路架构《AI Agent 开发平台资深技术专家 AI Agent 应用架构师 CTO 面试题库详解》
  • 下面设计实现的是:交换机Hlr指令处理任务模块。当然,在后续的业务发展过程中,还可能出现,其他类型指令的任务处理,所以根据“开闭”原则的定义,要抽象出一个接口类:BusinessEvent
  • Agent记忆中RAG难题,浙大MemGate盘活了
  • 终极指南:HS2-HF Patch - Honey Select 2游戏体验的完整革命
  • 智能合约开发中的威胁建模:代码生成前的安全基线构建
  • 生成式引擎优化(GEO)在酒店民宿行业的落地实践:对抗 OTA 流量截流
  • Adobe破解终极指南:三步免费激活Photoshop等专业软件
  • 【中小学AI人工智能教育】强化学习范例——平衡杆
  • Claude 桌面版(macOS / Windows)工具分享
  • DFT:IST和ROM BIST能不能同时跑?特别是在mission mode下
  • 多模态AI系统性能优化:从3.2秒到1.5秒的实战经验
  • 新160个CrackMe042-crackme、043-riijj_cm_20041121、044-tsrh-crackme逆向分析
  • 前端应用离线暂停更新策略:构建稳定可靠的渐进式部署方案
  • 第9章 MCP 协议与 Skills 工具生态《AI Agent 开发平台资深技术专家 AI Agent 应用架构师 CTO 面试题库详解》
  • 在C++基础上理解CSharp-6
  • AI 编译优化入门:算子融合不是为了少写几行代码
  • utpasswd命令详解:10个实用参数让密码管理更高效
  • SolidWorks_装配体设计5_自上而下设计
  • AI Agent 编排实战:别让多个智能体互相抢麦
  • 特种行业加固计算机配套的固态硬盘,兼容性问题通常出在哪里?