PHP 陣列、符號表、物件屬性、函數表等核心結構皆建立在 HashTable 之上,而其雜湊函式採用 DJBX33A (Daniel J. Bernstein, Times 33 with Addition)。本文深入剖析原始碼實作、優化技巧與神祕常數 5381 的由來。
演算法核心公式
hash(i) = hash(i-1) * 33 + str[i]
// 等價於
hash = (hash << 5) + hash + str[i] // hash * 33 = hash * 32 + hash = (hash << 5) + hash
Zend Engine 實作 (zend_hash.h)
static inline ulong zend_inline_hash_func(char *arKey, uint nKeyLength) {
register ulong hash = 5381;
// 循環展開:每次處理 8 個字元,減少迴圈開銷
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 fall-through 技巧
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;
}
return hash;
}
關鍵優化細節
| 技巧 | 說明 | 效益 |
| 位移代替乘法 | (hash << 5) + hash 替代 hash * 33 | CPU 位移指令比乘法快 1-3 個週期 |
| 循環展開 | 每迴圈處理 8 字元,手動展開 | 減少分支預測失敗、指令取得開銷 |
| switch fall-through | 餘數 1-7 字元無分支處理 | 避免迴圈判斷、極小字串零開銷 |
| register 關鍵字 | 建議編譯器將 hash 存入暫存器 | 減少記憶體存取 |
| inline 函式 | 消除函式呼叫開銷 | 熱路徑內聯展開 |
初始值 5381 的數學特性
5381 = 0x1505 (十六進位)
屬性:
1. 奇數 (LSB = 1) → 確保最低位參與運算
2. 質數 → 減少週期性模式
3. 虧數 (deficient) → 真因子和 < 自身 (1+7+769=777 < 5381)
4. 在 Bernstein 原始論文中建議用於 33x 雜湊
對比:Perl/Apache 使用初始值 0。5381 能產生更好的位元分佈,降低短字串衝突率。
PHP HashTable 結構圖解
HashTable (Bucket 陣列)
│
├── Bucket 0 → 雙向鏈表 (解決衝突)
│ ├── Bucket (key="abc", hash=...)
│ └── Bucket (key="xyz", hash=...) ← 同索引衝突
│
├── Bucket 1 → ...
│
└── 全域雙向鏈表 (遍歷用)
Bucket("abc") ⇄ Bucket("def") ⇄ Bucket("xyz") ...
- 第一層鏈表:同一雜湊索引的衝突鏈 (Separate Chaining)。
- 第二層鏈表:所有 Bucket 串聯,支援
foreach 依插入順序遍歷。
衝突解決與擴容
- 負載因子超過閾值 (預設 0.75) 觸發
zend_hash_do_resize,容量翻倍 (2 的冪次)。
- 重新雜湊所有現有元素,分散到新索引。
- PHP 7 引入 Compact HashTable:Bucket 直接內嵌陣列,消除指標間接存取,記憶體縮減 ~50%,速度提升 ~2x。
驗證雜湊分佈 (實驗)
<?php
function djb33a($str) {
$h = 5381;
for ($i = 0; $i < strlen($str); $i++)
$h = (($h << 5) + $h) + ord($str[$i]);
return $h;
}
// 測試 10000 隨機字串衝突率
$buckets = 10007; // 質數桶數
$collisions = 0;
$seen = [];
for ($i = 0; $i < 10000; $i++) {
$s = bin2hex(random_bytes(8));
$idx = djb33a($s) % $buckets;
if (isset($seen[$idx])) $collisions++;
$seen[$idx] = true;
}
echo "衝突數: $collisions / 10000 (" . ($collisions/100) . "%)";
// 典型結果: 衝突數 ~40-60 (理想泊松分佈 ~63)
對比:FNV-1a vs DJBX33A
| 特性 | DJBX33A | FNV-1a |
| 運算 | h = h*33 + c | h ^= c; h *= FNV_prime |
| 速度 | 極快 (位移+加法) | 快 (XOR+乘法) |
| 短字串分佈 | 優 (5381 初始值) | 良 |
| 硬體友善 | 無乘法指令相依 | 需乘法器 |
| 應用 | PHP, Perl, Berkeley DB, Apache | DNS, HashMap, 檔案系統 |
DJBX33A 以極簡運算、精巧優化、優異分佈,成為 PHP 核心雜湊的基石,展現系統級程式碼對效能極致追求的經典案例。
- 上一篇文章: PHP 防止頁面後退的 Session 單頁流程控制
- 下一篇文章: PHP 依 ISBN 查詢 Amazon 書誌資訊
Copyright ©
程式碼之家 All Rights Reserved