PHP 數組的靈魂是 有序哈希表。它既支持整數/字符串鍵混合、保持插入順序,又提供 O(1) 平均查找/插入/刪除。本文基於 PHP 7/8 源碼,解析 zend_array、Bucket、哈希函數 (DJBX33A)、沖突解決 (開放寻址法)、擴容縮容、遍歷機制、內存佈局優化。
// Bucket: 哈希表節點
struct _Bucket {
zval val; // 值 (內嵌 zval,PHP 7 優化)
zend_ulong h; // 哈希值 (整數鍵直接用值,字符串鍵預計算)
zend_string *key; // 字符串鍵指針 (整數鍵為 null)
};
// zend_array: 哈希表容器
struct _zend_array {
zend_refcounted_h gc;
uint32_t nTableMask; // = ~(nTableSize - 1),用於索引計算: idx = h & nTableMask
Bucket *arData; // 連續內存數組,存儲所有 Bucket
uint32_t nNumUsed; // 已用 Bucket 數 (含已刪除標記)
uint32_t nNumOfElements; // 實際有效元素數
uint32_t nTableSize; // arData 分配大小 (2 的冪次)
uint32_t nInternalPointer; // 內部指針 (current/next/prev)
zend_long nNextFreeElement; // 下一個可用整數鍵
dtor_func_t pDestructor; // 元素析構函數
};
arData 為連續分配的 Bucket 數組,非鏈表。CPU 緩存友好,遍歷極快 (線性掃描)。idx = h & nTableMask。沖突時線性探測 idx = (idx + 1) & nTableMask 直到空槽。無鏈表指針開銷,查找路徑短。arData 按插入順序排列。nInternalPointer 指向當前遍歷位置。foreach 直接線性掃描 arData,跳過 IS_UNDEF (已刪除標記)。h,key = null,節省 zend_string 分配。nTableMask 同步更新。nNumOfElements < nTableSize / 4 觸發 1/2 縮容 (避免頻繁擴縮容滯後)。IS_ARRAY_PACKED,迭代直接按索引訪問 arData[i],無需哈希計算。// 源碼: Zend/zend_string.h
static zend_always_inline zend_ulong zend_string_hash_val(const char *str, size_t len) {
zend_ulong hash = 5381;
for (size_t i = 0; i < len; i++) {
hash = ((hash << 5) + hash) + (zend_ulong)str[i]; // hash * 33 + c
}
return hash;
}
經典字符串哈希,分佈均勻、計算極快。PHP 7 在 zend_string 創建時預計算並緩存 h,避免重複計算。
zend_hash_add/zend_hash_updatenNumUsed >= nTableSize * 0.75)。idx = h & nTableMask。Z_TYPE(arData[idx].val) == IS_UNDEF)。val (zval)、h、key (字符串鍵需 zend_string_copy 增引用計數)。nNumUsed++, nNumOfElements++。key == null 且 h >= nNextFreeElement,更新 nNextFreeElement = h + 1。zend_hash_findidx = h & nTableMask。h 相等且 key 相等 (字符串鍵用 zend_string_equals)。&arData[idx].val,否則返回 null。zend_hash_delval (zval_ptr_dtor) 和 key (zend_string_release)。Z_TYPE(val) = IS_UNDEF (墓碑標記,不實際移動後續元素,保持探測鏈完整)。nNumOfElements--。檢查縮容條件。// 內部指針遍歷
ZEND_HASH_FOREACH_VAL(ht, val) {
// val 為 zval*
} ZEND_HASH_FOREACH_END();
// 或帶鍵
ZEND_HASH_FOREACH_KEY_VAL(ht, h, key, val) {
// h: 整數鍵, key: zend_string*, val: zval*
} ZEND_HASH_FOREACH_END();
實質是線性掃描 arData[0] ... arData[nNumUsed-1],跳過 IS_UNDEF。修改數組長度 (增刪) 會導致重置內部指針,foreach 行為未定義 —— 這就是為什麼不能在 foreach 中增刪當前數組。
| 場景 | 建議 | 原理 |
|---|---|---|
| 大量預插入 | 預分配大小: $arr = []; $arr[100000] = null; unset($arr[100000]); | 觸發一次性擴容到足夠大小,避免多次 rehash |
| 頻繁查找鍵是否存在 | isset($arr[$key]) / array_key_exists() | 直接哈希查找 O(1),比 in_array O(n) 快萬倍 |
| 只需值不需鍵 | array_values($arr) 重建打包數組 | 獲得 IS_ARRAY_PACKED,遍歷無哈希開銷 |
| 大數組傳遞給函數 | 按引用 function(&$arr) 或不修改則值傳遞 (COW) | 避免複製開銷;COW 寫時才複製 |
| foreach 中需刪除 | 收集待刪鍵,循環後再刪;或用 array_filter | 避免內部指針錯亂導致漏遍/重遍 |
PHP 哈希表 = 連續 Bucket 數組 + 開放寻址法 + 插入順序維護 + 打包優化。這種設計在「動態鍵值存儲」與「有序遍歷」雙重需求下取得了極佳平衡,是 PHP 數組性能強悍的根本原因。理解其內核機制,能指導你寫出更高效的數組操作代碼。