PHP 数组的底层实现是 Zend HashTable(在 PHP 7+ 中重命名为 zend_array,但本质仍是 HashTable),这是 PHP 引擎(Zend Engine)中最核心、最精巧的数据结构之一。理解它,就掌握了 PHP 动态性、灵活性与性能权衡的底层密码。


一、设计目标:为什么 PHP 需要这样的 HashTable?

PHP 数组必须同时满足 Web 脚本语言的三大需求

  1. 灵活性:无缝混合索引数组([1,2,3])与关联数组(['name'=>'John']
  2. 有序性foreach 遍历顺序 = 插入顺序(区别于传统哈希表)
  3. 高效性:平均 O(1) 的插入/查找,同时支持动态扩容

Zend HashTable = 有序哈希表(Ordered Hash Table) + 动态数组(Dynamic Array)的融合体


二、核心结构剖析(PHP 8.x)

zend_arrayHashTable 的 typedef,其核心字段如下:

typedef struct _zend_array {
    zend_refcounted_h gc;         // 引用计数,用于写时复制(CoW)
    uint32_t          nTableSize; // 哈希表桶(bucket)数量(2^n)
    uint32_t          nTableMask; // = nTableSize - 1(用于快速取模)
    uint32_t          nNumUsed;   // arData 中已使用的 Bucket 数(含 deleted)
    uint32_t          nNumOfElements; // 实际有效元素数量(不含 deleted)
    zend_long         nNextFreeElement; // 下一个数字索引(用于 $arr[] = x)
    Bucket           *arData;     // 核心:Bucket 数组(连续内存)
} zend_array;

关键组件:Bucket

typedef struct _Bucket {
    zval              val;        // 存储的值(zval)
    zend_ulong        h;          // 哈希值(数字键)或未使用(字符串键)
    zend_string      *key;        // 字符串键(NULL 表示数字键)
} Bucket;

🔑 arData 是一个连续的 Bucket 数组,既作为哈希表的“桶”,又作为保持插入顺序的“动态数组”


三、核心机制详解

1. 双视图:哈希表 + 有序列表

  • 哈希视图:通过 h & nTableMask 快速定位桶位置(冲突时用链地址法)
  • 顺序视图arData[0], arData[1], … 即 foreach 遍历顺序

🌰 示例:

$arr = ['a' => 1, 2 => 2, 'b' => 3];
  • arData[0]: key=‘a’, val=1
  • arData[1]: key=NULL, h=2, val=2
  • arData[2]: key=‘b’, val=3
  • 哈希表通过 hkey 的哈希值指向这些 Bucket

2. 数字索引与字符串索引统一处理

  • 数字键:直接存储 h = keykey = NULL
  • 字符串键:存储 zend_string* keyh = ZSTR_H(key)(字符串哈希值缓存)

3. 内存布局优化(PHP 7+)

  • 连续内存arData 是单一连续块,大幅提升 CPU 缓存命中率
  • 内联哈希:冲突链通过 arData 的索引而非指针维护(减少内存碎片)

4. 写时复制(Copy-on-Write, CoW)

  • 多个变量共享同一 zend_array 时,gc.refcount > 1
  • 修改前检查引用计数,若 >1 则复制整个 arData
  • 极大节省内存(尤其函数传参、数组字面量)

5. 动态扩容与缩容

  • 扩容:当 nNumOfElements > nTableSize * 2/3 时,nTableSize *= 2
  • 缩容:PHP 8.2+ 支持 array_repack() 释放未使用内存

四、性能特征(为什么 PHP 数组这么快?)

操作时间复杂度说明
[] 追加O(1) 摊销预分配空间,避免频繁 realloc
$arr[$key] 查找O(1) 平均哈希表定位
foreach 遍历O(n)连续内存,缓存友好
array_push / array_popO(1)利用 nNextFreeElement
大数组复制O(n)(但 CoW 优化)仅在写时复制

📊 实测
PHP 数组的遍历速度接近 C 数组,远超 Python dict / JS object(因连续内存)


五、PHP 版本演进:从 Zend HashTable 到 zend_array

版本关键改进
PHP 5.x哈希表 + 双向链表(内存碎片严重)
PHP 7.0重构为 连续 Bucket 数组(内存减半,速度翻倍)
PHP 7.4优化 packed array(纯数字索引数组)路径
PHP 8.0重命名为 zend_array,强化类型系统集成
PHP 8.2支持显式内存收缩(array_repack

💡 PHP 7 的 HashTable 重构是性能飞跃的关键(比 PHP 5 快 2~3 倍)


六、工程启示:如何写出高效 PHP 数组代码?

✅ 推荐实践

  • 预分配大小:大量追加时用 array_fill(0, $size, null) 初始化
  • 避免混合键类型:纯数字索引数组(packed array)性能更高
  • 利用 CoW:函数传参用 $arr 而非 &$arr(除非真要修改)
  • 慎用 array_merge:会创建新数组,大数组用 +foreach 赋值

❌ 反模式

  • 在循环中 $arr[] = ... 超大数组(频繁扩容)
  • 对大数组反复 array_slice / array_splice(触发复制)
  • 用字符串键模拟数字索引$arr["100"] vs $arr[100](后者更快)

七、与其他语言对比

语言底层结构有序性数字/字符串键
PHP有序哈希表(连续 Bucket)✅ 保持插入顺序完全统一
JavaScript对象:哈希表
数组:动态数组
❌ 对象无序(ES6+ Map 有序)分离(对象 vs 数组)
Pythondict:哈希表(3.7+ 有序)✅ 3.7+ 保持插入顺序统一(但类型严格)
JavaHashMap:哈希表
ArrayList:动态数组
❌ HashMap 无序分离(需不同类)

PHP 数组的“混合键+有序”设计,在动态语言中极为独特且实用


八、知识体系全景图

维度核心内容
结构zend_arrayBucketzval
机制哈希冲突处理、CoW、动态扩容、packed array 优化
性能O(1) 查找、连续内存、缓存友好
演进PHP 5 → 7 重构 → 8.x 优化
实践预分配、避免混合键、利用 CoW
哲学灵活性与性能的平衡:Web 脚本的最优解

总结

PHP 数组的底层 Zend HashTable(zend_array) 是一个为 Web 脚本场景量身定制的工程奇迹

  • 它用 连续内存 + 内联哈希 实现了有序性与 O(1) 性能的统一
  • 通过 写时复制 在灵活性与内存效率间取得平衡
  • 其设计哲学是:“让开发者无需关心底层,却能获得接近 C 的性能”

💡 理解 Zend HashTable,就理解了 PHP 为何能从“简单脚本语言”成长为“现代 Web 引擎”

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐