從PHP 的 zval 結構體可以看出 PHP 使用HashTable來保存數組信息,PHP的HashTable使用了一些技巧,這些技巧是PHP高效數組操作的直接原因,源代碼在PHP源代碼目錄 的Zend/zend_hash.h Zend/zend_hash.c 中。先來看看Zend HashTable的定義:
參數解釋:
nTableSize 哈希表的大小
nTableMask 數值上等於 nTableSize -1
nNumOfElements 記錄了當前 HashTable 中保存的記錄數
nNextFreeElement 指向下一個空閒的 Bucket (之後有解釋)
pInternalPointer
pListHead 指向 Bucket 列表頭部
pListTail 指向 Bucket 列表尾部
arBuckets
pDestructor 一個函數指針,在 HashTable 發生增、刪、改時自動調用,以完成某些清理工作。
persistent 是否是持久
nApplyCount
aApplyProtection 這兩個參數用於放置在遍歷時發生無限遞歸
可以看到Bucket 是一個雙向鏈表,參數解釋:
h 當元素使用數字索引時使用
nKeyLength 當使用字符串索引時,該選項表示字符串索引的長度,而字符串則保存在 Bucket 結構體的最後一個元素 arKey 中。儘管 arKey 被聲明爲一個只有一個元素的數組,但是這並不妨礙我們在其中保存字符串,因爲數組名可以看做指針,將 arKey 作爲結構體的最後一個元素則 Bucket 結構體就成了變長結構體,而該變長結構體的長度則需要 nKeyLength 的輔助才能確定,這是 C 語言中的常見技巧。
pNext指向具有相同 hash 值的下一個 bucket 元素,無論 HashTable 設計的如何完美,衝突都是難免的。當採用字符串索引時, h 成員變量存放的就是字符串索引的 hash 值。
pData指向保存的數據,如果數據本身又爲指針,則用 pDataPtr 來保存對應的指針,而辭此時 pData 則指向自身結構體的 pDataPtr 。
接着看Zend HashTable 的一些相關函數 :
#define HASH_PROTECT_RECURSION(ht) /
if ((ht)->bApplyProtection) { /
if ((ht)->nApplyCount++ >= 3) { /
zend_error(E_ERROR, "Nesting level too deep - recursive dependency?" ); /
} /
}
這個宏用於防止循環引用。
#define ZEND_HASH_IF_FULL_DO_RESIZE(ht) /
if ((ht)->nNumOfElements > (ht)->nTableSize) { /
zend_hash_do_resize(ht); /
}
該宏用於判斷HashTable 中的元素是否超過了 HashTable 表的大小,如果超過則擴展 HashTable 的大小,查看 zend_hash_do_resize 的代碼可以看到每次擴展大小都是成倍的。
看看Zend HashTable 是如何初始化的
ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, hash_func_t pHashFunction, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC)
{
uint i = 3; //這裏可以看出數組默認的初始化爲 8
Bucket **tmp;
SET_INCONSISTENT(HT_OK); // 用於調試
if (nSize >= 0×80000000) {
/* prevent overflow */
ht->nTableSize = 0×80000000;
} else {
while ((1U << i) < nSize) {
i++;
}
ht->nTableSize = 1 << i;
}
ht->nTableMask = ht->nTableSize - 1;
ht->pDestructor = pDestructor;
ht->arBuckets = NULL;
ht->pListHead = NULL;
ht->pListTail = NULL;
ht->nNumOfElements = 0;
ht->nNextFreeElement = 0;
ht->pInternalPointer = NULL;
ht->persistent = persistent;
ht->nApplyCount = 0;
ht->bApplyProtection = 1;
/* Uses ecalloc() so that Bucket* == NULL */
if (persistent) {
tmp = (Bucket **) calloc(ht->nTableSize, sizeof (Bucket *));
if (!tmp) {
return FAILURE;
}
ht->arBuckets = tmp;
} else {
tmp = (Bucket **) ecalloc_rel(ht->nTableSize, sizeof (Bucket *));
if (tmp) {
ht->arBuckets = tmp;
}
}
return SUCCESS;
}
可以看到HashTable 的大小被自動的初始化爲 2 的 n 次方, persistent 參數用於指示是否是“永久”方式分配內存,如果是則採用系統分配內存方法,否則採用ZendMM 的內存分配方式,關於 ZendMM 請搜索 PHP內存管理 的相關內容。
申請得到的bucket 指針內存塊都放在 HashTable 的 arBucket 中,可以把這段內存塊看成一個數組,數組中的每個元素都指向一個實際的 bucket 。
ZEND_API int _zend_hash_add_or_update(HashTable *ht, char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC)
{
ulong h;
uint nIndex;
Bucket *p;
IS_CONSISTENT(ht);
if (nKeyLength <= 0) {
#if ZEND_DEBUG
ZEND_PUTS( "zend_hash_update: Can’t put in empty key/n" );
#endif
return FAILURE;
}
//根據索引值和索引長度生成hash值
h = zend_inline_hash_func(arKey, nKeyLength);
//用hash值和nTableMask進行按位於運算,用於索引的快速定位
//按位於後的結果不可能大於nTableMask的值
//結合下面的代碼,可以看出這段代碼的巧妙
nIndex = h & ht->nTableMask;
p = ht->arBuckets[nIndex];
//如果p不爲NULL,則產生了hash衝突
while (p != NULL) {
if ((p->h == h) && (p->nKeyLength == nKeyLength)) {
if (!memcmp(p->arKey, arKey, nKeyLength)) {
if (flag & HASH_ADD) {
return FAILURE;
}
HANDLE_BLOCK_INTERRUPTIONS();
#if ZEND_DEBUG
if (p->pData == pData) {
ZEND_PUTS( "Fatal error in zend_hash_update: p->pData == pData/n" );
HANDLE_UNBLOCK_INTERRUPTIONS();
return FAILURE;
}
#endif
//到了這裏就說明是更新操作
//先調用原來的析構函數執行清理
if (ht->pDestructor) {
ht->pDestructor(p->pData);
}
UPDATE_DATA(ht, p, pData, nDataSize);
if (pDest) {
*pDest = p->pData;
}
HANDLE_UNBLOCK_INTERRUPTIONS();
return SUCCESS;
}
}
p = p->pNext;
}
//來到這裏說明是增加元素操作
p = (Bucket *) pemalloc( sizeof (Bucket) - 1 + nKeyLength, ht->persistent);
if (!p) {
return FAILURE;
}
memcpy(p->arKey, arKey, nKeyLength);
p->nKeyLength = nKeyLength;
INIT_DATA(ht, p, pData, nDataSize);
p->h = h;
CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]);
if (pDest) {
*pDest = p->pData;
}
HANDLE_BLOCK_INTERRUPTIONS();
CONNECT_TO_GLOBAL_DLLIST(p, ht);
ht->arBuckets[nIndex] = p;
HANDLE_UNBLOCK_INTERRUPTIONS();
ht->nNumOfElements++;
ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* If the Hash table is full, resize it */
return SUCCESS;
}
看到這裏就可以發現多數代碼都是類似的了,
#define CONNECT_TO_BUCKET_DLLIST(element, list_head) /
(element)->pNext = (list_head); /
(element)->pLast = NULL; /
if ((element)->pNext) { /
(element)->pNext->pLast = (element); /
}
這個宏用於將一個bucket 加入到 bucket 鏈表中
#define CONNECT_TO_GLOBAL_DLLIST(element, ht) /
(element)->pListLast = (ht)->pListTail; /
(ht)->pListTail = (element); /
(element)->pListNext = NULL; /
if ((element)->pListLast != NULL) { /
(element)->pListLast->pListNext = (element); /
} /
if (!(ht)->pListHead) { /
(ht)->pListHead = (element); /
} /
if ((ht)->pInternalPointer == NULL) { /
(ht)->pInternalPointer = (element); /
}
該宏用於將一個bucket 加入到 HashTable 的鏈表中
下面列出zend 封裝好的函數或者宏:
zend_hash_add_empty_element 給數組增加一個空元素
zend_hash_do_resize 擴大哈希表的大小
_zend_hash_index_update_or_next_insert 插入或者更新指定數字索引的元素
zend_hash_del_key_or_index 根據索引刪除HashTable 中的元素
zend_hash_apply 遍歷HashTable ,注意當中使用了兩個宏 HASH_PROTECT_RECURSION 和 HASH_UNPROTECT_RECURSION 來防止遍歷陷入死循環。
#define HASH_PROTECT_RECURSION(ht) /
if ((ht)->bApplyProtection) { /
if ((ht)->nApplyCount++ >= 3) { /
zend_error(E_ERROR, "Nesting level too deep - recursive dependency?" ); /
} /
}
#define HASH_UNPROTECT_RECURSION(ht) /
if ((ht)->bApplyProtection) { /
(ht)->nApplyCount–; /
}
zend_hash_reverse_apply 反向遍歷HashTable
zend_hash_copy 拷貝
_zend_hash_merge 合併
zend_hash_find 字符串索引方式查找
zend_hash_index_find 數值索引方法查找
zend_hash_quick_find 上面兩個函數的封裝
zend_hash_exists 是否存在索引
zend_hash_index_exists 是否存在索引
zend_hash_quick_exists 上面兩個方法的封裝
ZEND_API int zend_hash_num_elements(HashTable *ht)
{
IS_CONSISTENT(ht);
return ht->nNumOfElements;
}
獲得數組大小
爲了更加方便的操作HashTable,Zend將上面的宏做了進一步的封裝。