PHP数组底层基于哈希表实现,通过zend_array结构、DJBX33A哈希算法、链地址法处理冲突、双向链表维持插入顺序,并在满足条件时启用packed array优化整数索引访问。

PHP数组表面是灵活的键值容器,底层却依赖哈希表实现高效存取。当您执行 $arr['user'] = 'admin' 或 $arr[0] = 42 时,引擎并非简单写入内存,而是通过哈希计算定位存储位置,并维护插入顺序。以下是哈希表在PHP数组中发挥作用的核心机制:
一、哈希表结构体与内存布局
PHP数组对应一个 zend_array 结构(PHP 7+),其核心为 HashTable,包含桶数组 arData、哈希掩码 nTableMask、元素数量 nNumOfElements 和容量 nTableSize。arData 指向一块连续分配的内存,每个单元为 Bucket,大小固定为 sizeof(Bucket)。
1、nTableMask 恒等于 nTableSize - 1,且 nTableSize 始终为 2 的幂次,使哈希索引可通过位运算 hash & nTableMask 替代耗时的取模运算。
2、每个 Bucket 存储 zval(值)、h(整数键或字符串哈希值)、key(字符串键指针)及 next(冲突链表指针)。
立即学习“PHP免费学习笔记(深入)”;
3、Bucket 内存按顺序连续排列,但逻辑上通过哈希索引和链表指针组织,兼顾随机访问与遍历效率。
二、键的哈希计算与槽位映射
哈希函数决定键如何映射到 arData 的物理位置。PHP 使用 DJBX33A 算法处理字符串键,生成 64 位哈希值;整数键则直接作为 h 字段使用,跳过哈希计算。该设计确保两类键均能快速参与索引定位。
1、对字符串键 'config',引擎调用 zend_string_hash_val() 获取哈希值,再与 nTableMask 执行按位与,得到初始槽位索引。
2、对整数键 123,直接将 123 赋给 Bucket 的 h 字段,并用相同位运算定位槽位。
3、若目标槽位已被占用,新 Bucket 不覆盖原数据,而是通过 next 字段链接至该槽位的冲突链表头部。
三、哈希冲突的链地址法处理
当多个键经哈希后落入同一槽位,PHP 采用链地址法(separate chaining),而非开放寻址法。每个槽位可承载多个 Bucket,形成以 arData[i] 为头节点的单向链表,避免探测式查找带来的性能退化和空间浪费。
1、插入冲突键时,新 Bucket 的 next 指向当前槽位首 Bucket,随后 arData[i] 指针更新为指向新 Bucket,实现 O(1) 头插。
2、查找时,引擎先定位 arData[i],再沿 next 链表逐个比对 h 值与 key 内容(字符串需 memcmp,整数键直接比较 h),确保语义一致性。
3、删除操作仅标记 Bucket 为“已删除”(via ZVAL_UNDEF),不调整链表结构,待后续插入或遍历时统一清理。
四、双向链表保障插入顺序
PHP 数组必须保持 foreach 遍历时的插入顺序,这由 Bucket 中隐式维护的双向链表实现。该链表独立于哈希索引,通过 pListHead 和 pListTail 指针管理,不依赖哈希槽位分布。
1、每次插入新元素,无论是否发生哈希冲突,该 Bucket 均被追加至双向链表尾部,pListTail->next 指向新 Bucket,新 Bucket 的 prev 指向原尾部。
2、遍历时引擎跳过哈希表,直接从 pListHead 出发,沿 next 指针线性访问,严格遵循写入序列。
3、删除元素时,仅断开其在双向链表中的前后指针,不移动其他 Bucket 位置,维持剩余元素顺序不变。
五、packed array 的整数索引优化路径
当数组满足全部为非负连续整数键(如 0,1,2,…,n−1)、无空洞、且元素数等于容量时,PHP 7+ 启用 packed array 模式。此时哈希表退化为纯索引数组,绕过哈希计算与链表跳转,极大提升数值索引访问性能。
1、判断条件由引擎在每次插入/删除后动态校验:nNumOfElements == nTableSize && nNextFreeElement == nNumOfElements 且所有键均为整数并连续。
2、访问 $arr[5] 时,直接计算 arData + 5 * sizeof(Bucket) 地址偏移,无需哈希、无需链表遍历。
3、一旦插入字符串键或执行 unset($arr[2]) 造成空洞,packed array 状态立即失效,恢复为通用哈希表模式。



















