PHP 7.4.30 的 HashTable 是自研开放寻址哈希表,采用“一结构三段连续内存”设计(Header+Bucket数组+hash数组),支持有序插入、线性探测、DELETED_IDX标记删除、整数/字符串键统一哈希,兼顾性能、内存友好与遍历顺序。

PHP 7.4.30 中的 HashTable 是其核心数据结构,用于实现数组(array)、对象属性存储、符号表(symbol table)、函数/类注册表等。它不是标准 C 的哈希表,而是 PHP 自研的、高度优化的开放寻址(open addressing)哈希表,结合了双向链表(用于遍历与迭代)和紧凑内存布局设计。
核心目标是:高效插入/查找/删除 + 保持插入顺序 + 支持稀疏索引 + 内存友好。它不依赖指针链式结构(避免频繁 malloc),而用连续内存块 + 偏移量 + 标志位实现。
内存布局:一个结构体,三块连续内存
一个 `HashTable` 实例(`struct _zend_array`)实际由三段紧邻内存组成:
- Header:`zend_array` 结构体本身(24 字节,含 `nTableSize`、`nNumUsed`、`nNumOfElements`、`arData` 指针等)
- Data array:`Bucket` 数组(`arData`),大小为 `nTableSize`,每个 `Bucket` 占 32 字节(x86_64),包含 key(`zend_string*` 或整数 hash)、val(`zval`)、h(hash 值)、next(用于冲突链)
- Hash table:`uint32_t hash[]` 数组(紧跟在 `arData` 后),长度也是 `nTableSize`,存每个槽位对应 `Bucket` 在 `arData` 中的索引(或 `INVALID_IDX`)
这种“一结构三段内存”设计让 `HashTable` 可整体 malloc 一次,提升缓存局部性,也便于 GC 管理。
立即学习“PHP免费学习笔记(深入)”;
键的存储与哈希计算
PHP 数组支持整数键和字符串键,统一转为 `zend_ulong h`(即 hash 值)参与寻址:
- 整数键:直接作为 `h`(`h = (zend_ulong)key`),但会做掩码处理(`h & (nTableSize - 1)`)
- 字符串键:调用 `zend_string_hash_val(key)`,使用 DJBX33A 变种算法(初始值 5381,循环 `hash * 33 + c`),结果再取模
注意:`HashTable` 不保存原始 key 字符串的副本 —— 字符串 key 全部用 `zend_string*` 引用(带 refcount),整数 key 则直接存于 `Bucket.key` 为 `NULL`,靠 `Bucket.h` 和 `Bucket.val.u2.key_ptr`(若为 interned string)或隐式判断区分。
插入与查找:线性探测 + 预留空槽
采用开放寻址中的线性探测(linear probing),但做了关键优化:
- 哈希后得到初始位置 `idx = h & (nTableSize - 1)`
- 从 `idx` 开始向后扫描 `hash[]` 数组,找第一个 `hash[i] == INVALID_IDX` 或 `hash[i] == h`
- 若找到 `hash[i] == h`,则比对 key(整数比 `h`,字符串比 `zend_string*` 地址或内容)确认是否命中
- 若找到 `INVALID_IDX`,则在此处插入新 `Bucket`,并更新 `hash[i] = h`
- 为加速删除后查找,删除时并不清空 `hash[i]`,而是设为 `DELETED_IDX`(0xFFFFFFFF),后续插入可复用,查找时跳过
`nNumUsed` 记录 `arData` 中已用 `Bucket` 数(含被删的),`nNumOfElements` 才是真实元素个数;`nTableSize` 总是 2 的幂(如 8, 16, 32…),保证位运算取模高效。
有序性与迭代:Bucket 数组即插入顺序
所有插入的 `Bucket` 按时间顺序追加到 `arData` 末尾(`nNumUsed` 指向下一个空位),`hash[]` 只负责快速定位,不打乱顺序。因此:
- `foreach ($arr as $k => $v)` 天然按插入顺序遍历
- 内部用 `pInternalPointer`(指向当前 `Bucket*`)+ `nInternalPointer`(索引)管理迭代器
- `Bucket` 中隐含 `pNext` / `pLast` 字段(通过 `arData[i].val.u2.next` 复用 zval 的 union 空间),构成逻辑双向链表,供 `reset()`/`next()` 等操作使用
这种设计使 `foreach` 遍历复杂度为 O(1) 每步,且无需额外链表内存开销。
不复杂但容易忽略:`HashTable` 的扩容不是简单 realloc,而是申请新两倍大小的内存块,重新哈希所有有效元素(跳过 `DELETED_IDX` 和空槽),再原子替换 `arData` 和 `hash` 指针;缩容同理,但仅当负载率低于 1/4 且 size > 32 时触发。



















