PHP中 的 HashTable

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將上面的宏做了進一步的封裝。

發佈了168 篇原創文章 · 獲贊 5 · 訪問量 54萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章