程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> PHP內核探索之變量(3)- hash table,hashtable

PHP內核探索之變量(3)- hash table,hashtable

編輯:關於PHP編程

PHP內核探索之變量(3)- hash table,hashtable


       在PHP中,除了zval, 另一個比較重要的數據結構非hash table莫屬,例如我們最常見的數組,在底層便是hash table。除了數組,在線程安全(TSRM)、GC、資源管理、Global變量、ini配置管理中,幾乎都有Hash table的蹤跡(上一次我們也提到,符號表也是使用Hash table實現的)。那麼,在PHP中,這種數據有什麼特殊之處,結構是怎麼實現的? 帶著這些問題,我們開始本次的內核探索之旅。

      本文主要內容:

一、Hash table的基本介紹和背景知識

1.    基本定義

       Hash table,又叫哈希表,散列表,Hash表,維基百科上對哈希表的定義是:"散列表,是根據關鍵字(Key value)而直接訪問在內存存儲位置的數據結構。也就是說,它通過把鍵值通過一個函數的計算,映射到表中一個位置來訪問記錄,這加快了查找速度。這個映射函數稱做散列函數,存放記錄的數組稱做散列表。”。提取文中的主干,我們可以得出如下信息:

(1).Hash table是一種數據結構。

(2).這種數據結構是普通數組的擴展。

(3).這種數據結構通過key->value的映射關系,使得插入和查找的效率很高(數組可以直接尋址,可在O(1)的時間內訪問任意元素)。

      我們知道,在一般的數組、線性表、樹中,記錄在結構中的位置是相對隨機的,即記錄和關鍵字之間不存在直接的、確定的對應關系。在這些結構中,要查找和插入關鍵字,常常需要進行一系列的比較,查找的效率通常是O(n)或者O(lgn)的。而Hash table通過Hash函數建立了關鍵字和記錄之間的對應關系,使得普通的查找和插入操作可以在O(1)(平均時間復雜度)的時間內完成,這顯然是最理想的查找方式。

2.    Hash函數

      如上所述,Hash函數建立了關鍵字和記錄之間的對應關系,即:Record = Hash(key) , 這種對應關系如下所示:


理論上,哈希函數可以是任何函數如Crc32, unique_id,MD5,SHA1或者用戶自定義的函數。這個函數的好壞直接關系到Hash table的性能(考慮沖突和查找的性能)。這裡列舉了幾個常見的Hash函數和對應的實現,有興趣的童鞋可以看看。一個典型的字符串Hash算法如下:

function hash( $key ){
    $result = 0;
    $len = strlen($key);

    for($i = 0;$i < $len; $i++ ){
        $result += ord($key{$i}) * ((1 << 5) + 1);
    }
    return $result;
}

3.沖突解決

       在理想的情況下,我們期望任何關鍵字計算出的Hash值都是唯一的,這樣我們便可以通過Hash(key)這種方式直接定位到要查找的記錄。但不幸的,幾乎沒有一個Hash函數可以滿足這樣的特性(即使有這樣的Hash函數,也可能很復雜,無法在實際中使用)。也就是說,即使是精心設計的Hash函數,也經常會出現key1 != key2 但是hash(key1) = hash(key2)的情況,這便是Hash沖突(Hash碰撞)。解決Hash碰撞的主要方法有多種(見這裡),作為示例,我們只簡單討論下鏈接法解決沖突。這種方法的基本思想是:在哈希表出現沖突時,使用鏈表的形式鏈接所有具有相同hash值的記錄,而哈希表中只保存鏈表的頭指針。PHP底層的Hash table,便是使用鏈表(雙向鏈表)來解決hash沖突的。關於這一點,後續會有詳細的介紹。

       引入鏈表之後,Hash table的結構如下所示:

 

       一個簡單的Hash table的實現如下:

Class HashTable{
    private $buckets = null; 	
	
	/* current size */
	private $size = 0;    
	
	/* max hashtable size */
	private $max = 2048;
	
	private $mask = 0;
	
	public function __construct($size){
		$this->_init_hash($size);
	}
	
	/* hashtable init */
	private function _init_hash($size){
		if($size > $this->max){
			$size = $this->max;
		}

		$this->size     = $size;
		$this->mask    = $this->size - 1;
	
		// SplFixedArray is faster when the size is known
		// see http://php.net/manual/en/class.splfixedarray.php
		$this->buckets = new SplFixedArray($this->size);
	}

    public function hash( $key ){
        $result = 0;
        $len  = strlen($key);
 
        for($i = 0;$i < $len; $i++ ){
            $result += ord($key{$i}) * ((1 << 5) + 1);
        }
        return $result % ($this->size);
    }

    /* 拉鏈法 */
    public function insert( $key, $val ){
        $h = $this->hash($key);
        if(!isset($this->buckets[$h])){
            $next = NULL;
        }else{
            $next = $this->bucket[$h];
        }
        $this->buckets[$h] = new Node($key, $val, $next);
    }
  
    /* 拉鏈法 */
    public function lookup( $key ){
        $h   = $this->hash($key);
        $cur = $this->buckets[$h];
 
        while($cur !== NULL){
            if( $cur->key == $key){
                return $cur->value;
            }
            $cur = $cur->next;
        }
        return NULL;
    }
}

Class Node{
    public $key;
    public $value;
    public $next = null;
 
    public function __construct($key, $value, $next = null){
        $this->key   = $key;
        $this->value = $value;
        $this->next  = $next;
    }
}

$hash = new HashTable(200);
$hash->insert('apple','this is apple');
$hash->insert('orange','this is orange');
$hash->insert('banana','this is banana');
echo $hash->lookup('apple');

       我們知道,在PHP中,數組支持k->v這樣的關聯數組,也支持普通的數組。不僅支持直接尋址(根據關鍵字直接定位),而且支持線性遍歷(foreach等)。這都要歸功於Hash table這一強大和靈活的數據結構。那麼,在PHP底層,Hash table究竟是如何實現的呢?我們一步步來看。

二、PHP中Hash table的基本結構和實現

1.   基本數據結構

在PHP底層,與Hash table相關的結構定義、算法實現都位於Zend/zend_hash.c和Zend/zend_hash.h這兩個文件中。PHP 的hash table實現包括兩個重要的數據結構,一個是HashTable,另一個是bucket.前者是hash table的主體,後者則是構成鏈表的每個“結點”,是真正數據存儲的容器。

(1)   HashTable的基本結構

定義如下(zend_hash.h):

typedef struct _hashtable {
    uint nTableSize;
    uint nTableMask;
    uint nNumOfElements;
    ulong nNextFreeElement;
    Bucket *pInternalPointer;   /* Used for element traversal */
    Bucket *pListHead;
    Bucket *pListTail;
    Bucket **arBuckets;
    dtor_func_t pDestructor;
    zend_bool persistent;
    unsigned char nApplyCount;
    zend_bool bApplyProtection;
#if ZEND_DEBUG
    int inconsistent;
#endif
} HashTable;

這是一個結構體,其中比較重要的幾個成員:

nTableSize 這個成員用於標明Hash表的大小,在hash表初始化操作的時候,會設定nTableSize的大小,而在hash表擴容的時候,也會相應調整這個數值的大小。注意這個數值並不是hash表中元素的個數。

nTableMask 是一個“掩碼”,主要用於快速計算一個元素的索引(nIndex = h & ht->nTableMask,在一般的Hash函數中,是通過模運算來確定索引的,但顯然,位運算比模運算效率要高),在arBuckets初始化之後,該值默認固定為nTableSize – 1;

nNumOfElements 這個成員保存了hashtable中保存的元素的個數,通常情況下,我們在PHP腳本中使用count($arr)與這個結果是一致的(參見ext/standard/array.c)

nNextFreeElement  這個字段記錄下一個可用的索引位置,我們在腳本中使用$array[] = 'key'的時候,就是使用nNextFreeElement給出的索引值(zend_hash.c):

if (flag & HASH_NEXT_INSERT) {
    h = ht->nNextFreeElement;
}

pInternalPointer 這是一個指針。在PHP腳本中,我們使用current,next,key,end等 與數組相關的操作時,都是使用pInternalPointer這一指針來完成的。

pListHead 和pListTail  PHP底層實際上維護了兩個重要的數據結構,除了hash表(以及用於解決沖突的雙鏈表),還有一個雙向鏈表用於hash表元素的線性掃描。pListHead和pListTail便指向這個雙鏈表的表頭和表尾。

arBuckets 這是一個bucket *類型的數組,數組中每個元素都是一個bucket* 的指針,具有相同hash值的元素通過bucket的pNext和pLast指針連接成一個雙鏈表(這個雙鏈表與前面說的用於線性遍歷的雙鏈表並不是一個東西)。因此,bucket是實際存儲數據的容器。

nApplyCount和bApplyProtection 提供了一種保護機制,主要是用於防止循環引用導致的無限遞歸。

persistent 這是一個布爾變量,該變量會影響到內存分配的方式,這涉及到PHP內存管理的一些知識,我們暫時不做更多解釋,詳細的可以參考: 

http://cn2.php.net/manual/en/internals2.memory.persistence.php

(2)另一個數據結構是Bucket

該結構的定義為:

typedef struct bucket {
    ulong h;
    uint nKeyLength;
    void *pData;
    void *pDataPtr;
    struct bucket *pListNext;
    struct bucket *pListLast;
    struct bucket *pNext;
    struct bucket *pLast;
    const char *arKey;
} Bucket;

其中:

h ,arKey,nKeyLength PHP數組中,有兩類不同的索引,一類是數字索引,這與C中的數組非常類似(如$arr = array(1=>'cont')), 另一類是字符串索引,也就是使用關鍵詞作為數組項的索引(如$arr = array('index'=>'cont');).這兩類索引在PHP底層是通過不同的機制來區分的:對於數字型索引,直接使用h作為hash值,同時,arKey=NULL 且nKeyLength=0, 而對於字符串索引,arKey保存字符串key, nKeyLength保存該key的長度,h則是該字符串通過hash函數計算後的hash值。這樣,在PHP中,實際上通過h, arKey, nKeyLength來唯一確定數組中的一個元素的,這從zend_hash_key這個結構體的定義也可以看出來:

typedef struct _zend_hash_key {
    const char *arKey;
    uint nKeyLength;
    ulong h;
} zend_hash_key;

而確定數組元素在hashtable中的位置則是通過h & ht->nTableMask 來實現的:

/* 字符串型索引 */
h = zend_inline_hash_func(arKey, nKeyLength);
nIndex = h & ht->nTableMask;

 
/* 數字型索引-append $arr[] = 'test';這種形式 */
if (flag & HASH_NEXT_INSERT) {
    h = ht->nNextFreeElement;
}

/* 指定數字索引時直接使用h */
nIndex = h & ht->nTableMask;

pData和pDataPtr  通常情況下,Bucket中的數據是保存在pData指針指向的內存空間的。但是也有例外,例如保存的是一個指針。這時,pDataPtr指向該指針,而pData指向pDataPtr。這從INIT_DATA這個宏定義可以看出來:

#define INIT_DATA(ht, p, pData, nDataSize);                             \
    if (nDataSize == sizeof(void*)) {                                   \
        memcpy(&(p)->pDataPtr, pData, sizeof(void *));                  \
        (p)->pData=&(p)->pDataPtr;                                      \
    }else{                                                              \
        (p)->pData = (void *) pemalloc_rel(nDataSize, (ht)->persistent);\
		if(!(p)->pData){                                                \
			pefree_rel(p, (ht)->persistent);                            \
			return FAILURE;                                             \
		}                                                               \
		memcpy((p)->pData,pData,nDataSize);                             \
		(p)->pDataPtr=NULL;                                             \
    }

pListNext和pListLast,pNext和pLast 前面已經介紹過,pListNext和pListLast構成了用於遍歷的整個雙鏈表。而pNext和pLast則是在出現hash沖突時,用於鏈接具有相同hash值的Bucket。這兩種雙鏈表的結構分別如下圖所示:

a. 發生hash沖突時的雙鏈表:

b. 用於全局的雙鏈表:

需要注意的是,這兩種雙鏈表結構並不是單獨存在,而是相互關聯的。在HashTable的相關操作中,需要同時維護這兩種鏈表:

 

可以看出,PHP的hashTable相當復雜,正是這種復雜性,使得PHP的數組操作有很大的靈活性(PHP中數組可以用作數組、棧、隊列,可以說非常便利)

三、HashTable的實現

1.   HashTable相關宏定義

為了方便操作HashTable, PHP底層定義了很多的宏,這些宏包括:

(1).  CONNECT_TO_BUCKET_DLLIST(element, list_head)

該宏用於把元素插入Bucket的雙鏈表的頭部,也就是說,在發生沖突時,新插入的元素總是位於Bucket鏈表的頭部。該宏的定義為:

#define CONNECT_TO_BUCKET_DLLIST(element, list_head)       \
   (element)->pNext = (list_head);                         \
   (element)->pLast = NULL;                                \
   if ((element)->pNext) {                                 \
       (element)->pNext->pLast = (element);                \
   }

(2). CONNECT_TO_GLOBAL_DLLIST(element, ht)

與上述不同,這個是將元素插入到全局遍歷的雙鏈表的末尾,這個雙鏈表類似隊列的作用,它保證了我們遍歷數組時的正確順序。該宏的定義是:

 1 #define CONNECT_TO_GLOBAL_DLLIST(element, ht)               \
 2     (element)->pListLast = (ht)->pListTail;                 \
 3     (ht)->pListTail = (element);                            \
 4     (element)->pListNext = NULL;                          \
 5     if ((element)->pListLast != NULL) {                     \
 6         (element)->pListLast->pListNext = (element);        \
 7     }                                                                           \
 8 
 9     if (!(ht)->pListHead) {                                             \
10         (ht)->pListHead = (element);                               \
11     }                                                                            \
12 
13     if ((ht)->pInternalPointer == NULL) {                          \
14         (ht)->pInternalPointer = (element);                      \
15     }

(3). HASH_PROTECT_RECURSION(ht)

這個宏主要用於防止HashTable被遞歸遍歷時深度過大,是一種保護機制

#define HASH_PROTECT_RECURSION(ht) \
    if ((ht)->bApplyProtection) { \
        if ((ht)->nApplyCount++ >= 3) { \
            zend_error(E_ERROR, "Nesting level too deep - recursive dependency?");\
        } \
    }

(4). ZEND_HASH_IF_FULL_DO_RESIZE(ht)

HashTable的大小並不是固定不變的,當nNumOfElements > nTableSize時,會對HashTable進行擴容,以便於容納更多的元素,這便是通過該宏實現的(實際上是調用zend_hash_do_resize來實現的)。該宏定義為:

#define ZEND_HASH_IF_FULL_DO_RESIZE(ht)             \
    if ((ht)->nNumOfElements > (ht)->nTableSize) {  \
        zend_hash_do_resize(ht);                    \
    }

(5). INIT_DATA(ht, p, pData, nDataSize)

這裡實際上有兩種情況,如果要保存的數據本身是一個指針,則pDataPtr保存該指針,並且將pData指向pDataPtr的地址:

if (nDataSize == sizeof(void*)) {
    memcpy(&(p)->pDataPtr, pData, sizeof(void *));
    (p)->pData = &(p)->pDataPtr;
}

否者保存的是普通的數據,則申請分配nDataSize字節的內存,並將pData指向內存的內容復制到p->pData的內存。這裡,復制都是通過memcpy來進行的,因為它的src和dest的指針都是void *的,因此可以復制幾乎任何類型的數據:

else {
    (p)->pData = (void *) pemalloc_rel(nDataSize, (ht)->persistent);
    if (!(p)->pData) {
        pefree_rel(p, (ht)->persistent);
        return FAILURE;
    }
    memcpy((p)->pData, pData, nDataSize);
    (p)->pDataPtr=NULL;
}

整個宏定義為:

#define UPDATE_DATA(ht, p, pData, nDataSize)                                            \
    if (nDataSize == sizeof(void*)) {                                                   \
        if ((p)->pData != &(p)->pDataPtr) {                                             \
           pefree_rel((p)->pData, (ht)->persistent);                                   \
        }                                                                               \
        memcpy(&(p)->pDataPtr, pData, sizeof(void *));                                  \
        (p)->pData = &(p)->pDataPtr;                                                    \
    } else {                                                                            \
        if ((p)->pData == &(p)->pDataPtr) {                                             \
            (p)->pData = (void *) pemalloc_rel(nDataSize, (ht)->persistent);            \
            (p)->pDataPtr=NULL;                                                         \
        } else {                                                                        \
            (p)->pData = (void *) perealloc_rel((p)->pData, nDataSize, (ht)->persistent);\
            /* (p)->pDataPtr is already NULL so no need to initialize it */             \
        }                                                                               \
        memcpy((p)->pData, pData, nDataSize);                                           \
    }

(6). UPDATE_DATA(ht, p, pData, nDataSize)

與INIT_DATA類似,不同的是,需要對之前的內存塊做更多的處理(例如之前pData保存的實際的數據,但是update之後保存的是指針,則需要釋放原來申請的內存,否者就會造成內存洩露,相反,如果之前保存的是指針數據,update之後保存的是普通的數據,則pDataPtr要設置為NULL,同時為pData分配新的內存空間),該宏的定義為:

#define UPDATE_DATA(ht, p, pData, nDataSize)                                            \
    if (nDataSize == sizeof(void*)) {                                                   \
        if ((p)->pData != &(p)->pDataPtr) {                                             \
            pefree_rel((p)->pData, (ht)->persistent);                                   \
        }                                                                               \
        memcpy(&(p)->pDataPtr, pData, sizeof(void *));                                  \
        (p)->pData = &(p)->pDataPtr;                                                    \
    } else {                                                                            \
        if ((p)->pData == &(p)->pDataPtr) {                                             \
            (p)->pData = (void *) pemalloc_rel(nDataSize, (ht)->persistent);            \
            (p)->pDataPtr=NULL;                                                         \
        } else {                                                                        \
            (p)->pData = (void *) perealloc_rel((p)->pData, nDataSize, (ht)->persistent);   \
            /* (p)->pDataPtr is already NULL so no need to initialize it */             \
        }                                                                               \
        memcpy((p)->pData, pData, nDataSize);                                           \
    }

(7). CHECK_INIT(ht)

在調用_zend_hash_init()為hash table初始化之後,實際上arBuckets並沒有分配內存空間,且沒有設置nTableMask的值。CHECK_INIT會檢查arBuckets是否已經初始化(nTableMask==0表示未初始化),如果沒有初始化,則要為arBuckets分配內存空間,同時設置nTableMask的值為nTableSize – 1.該宏定義為:

#define CHECK_INIT(ht) do {                                             \
    if (UNEXPECTED((ht)->nTableMask == 0)) {                                \
        (ht)->arBuckets = (Bucket **) pecalloc((ht)->nTableSize, sizeof(Bucket *), (ht)->persistent);   \
        (ht)->nTableMask = (ht)->nTableSize - 1;                        \
    }                                                                   \
} while (0)

2.   哈希函數

       寫這篇文章的時候,發現鳥哥已經寫了一篇《PHP中的hash算法》,裡邊對hash的算法、思想等都做了比較詳細的解答,這裡就不在做過多的解釋,只說一點:unrolled。unrolled本身是展開的意思,對於nKeyLength長度的key, PHP的hash算法會以8為單位做unrolled,也就是這樣的形式:

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++;
}

那為什麼不直接用循環呢?

比如說:

for(;nKeyLength > 0; nKeyLength--){
     hash = ((hash << 5) + hash) + *arKey++;
}

這樣其實是沒有問題的,而unroll的原因自然是效率更高:對CPU而言,一般順序執行的指令比循環要快(後者在匯編指令中表現為JMP, JNZ等跳轉,以及循環之前的比較)。同時,對於8位以下的字符串索引,會有更好的效率。

順便貼出hash函數的實現源碼:

/* 
 * 1. inline static 是為了提高效率
 * 2. const限定arKey, 表明在函數中arKey的內容不應該不修改
 */

static inline ulong zend_inline_hash_func(const char *arKey, uint nKeyLength)
{       
     /* 3.register變量,也是為了提高效率 */
    register ulong hash = 5381;

    /* 4. variant with the hash unrolled eight times */
    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 (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;
EMPTY_SWITCH_DEFAULT_CASE()
    }
    /* 5. 返回的hash值並沒有經過取模運算 */
    return hash;
}

3.   初始化、添加/更新和查找、刪除等API

(1). 初始化

_zend_hash_init用於hash table的初始化操作(主要包括對hashTable這個結構體的數據成員賦初值)。調用_zend_hash_init之後,nTableMask默認為0(之後再CHECK_INIT時被賦值為nTableSize-1), nTableSize被賦值為大於nSize的最小的2的整數次方,並且nTableSize最小為8,最大為0x80000000,且在_zend_hash_init之後,arBuckets是沒有分配內存空間的(也是在CHECK_INIT時分配的)。nTableMask用於快速計算hash值對應的索引,因為它有一個特性,即nTableMask = 2^n – 1,展開成二進制之後,所有位都是1,因而通過nIndex = h & nTableMask可以快速得到索引位置。該函數的實現源碼(不同版本的具體實現有不同,本文的PHP版本是5.4.24):

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)
{ 
    /* hashTable最小size為 1<<3 = 8 */
    uint i = 3;

    SET_INCONSISTENT(HT_OK);
    if (nSize >= 0x80000000) {
        /* prevent overflow */
        ht->nTableSize = 0x80000000;
    } else {
        while ((1U << i) < nSize) {
            i++;
        }
        ht->nTableSize = 1 << i;
    }

    ht->nTableMask = 0; /* 0 means that ht->arBuckets is uninitialized */
    ht->pDestructor = pDestructor;
    ht->arBuckets = (Bucket**)&uninitialized_bucket;
    ht->pListHead = NULL;
    ht->pListTail = NULL;
    ht->nNumOfElements = 0;
    ht->nNextFreeElement = 0;
    ht->pInternalPointer = NULL;
    ht->persistent = persistent;
    ht->nApplyCount = 0;
    ht->bApplyProtection = 1;
    return SUCCESS;
}

(2). 查找元素。

對於字符串索引和數字索引,分別提供了zend_hash_find和zend_hash_index_find兩種查找方式。這兩種方式並沒有本質的不同,都是在計算hash值之後,尋找元素在對應Bucket中的位置。對字符串索引,確定相同的條件是:p->arKey == arKey ||((p->h == h) && (p->nKeyLength == nKeyLength) && !memcmp(p->arKey, arKey, nKeyLength)),即要麼arKey和p->arKey指向的同一塊內存,要麼h,nKeyLength和arKey指向的內容完全一致,才能確定為相同。而對於數字型索引,只需要(p->h == h) && (p->nKeyLength == 0)即可。這兩種查找的實現如下:

/* 數字型索引的查找 */
ZEND_API int zend_hash_index_find(const HashTable *ht, ulong h, void **pData)
{
    uint nIndex;
    Bucket *p;

    IS_CONSISTENT(ht);
     
     /* 計算索引 */
    nIndex = h & ht->nTableMask;
    p = ht->arBuckets[nIndex];
   
    /* 遍歷雙鏈表,一旦找到立即返回 */
    while (p != NULL) {
        if ((p->h == h) && (p->nKeyLength == 0)) {
            *pData = p->pData;
            return SUCCESS;
        }
        p = p->pNext;
    }

     /* 如果遍歷完雙鏈表,沒有找到,那麼查找失敗 */
    return FAILURE;
}

/* 字符串索引的查找 */ ZEND_API int zend_hash_find(const HashTable *ht, const char *arKey, uint nKeyLength, void **pData) { ulong h; uint nIndex; Bucket *p; IS_CONSISTENT(ht); /* 字符串索引需要先計算字符串的hash值 */ h = zend_inline_hash_func(arKey, nKeyLength); nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; /* Bucket雙鏈表中查找,一旦找到,立即返回,注意查找成功的條件 */ while (p != NULL) { if (p->arKey == arKey || ((p->h == h) && (p->nKeyLength == nKeyLength) && !memcmp(p->arKey, arKey, nKeyLength))) { *pData = p->pData; return SUCCESS; } p = p->pNext; } /* 查找失敗 */ return FAILURE; }

(3).插入元素

在PHP腳本中,有三種形式可以在當前數組中插入元素,如:

$arr = array();
$arr['index'] = 'cont';
$arr[2]       = 'test';
$arr[]        = 10; 

這三種插入方式分別是:"字符串索引插入","數字索引插入","下一個可用位置插入",在實現中,"字符串索引插入"對應_zend_hash_add_or_update,而後兩種對應_zend_hash_index_update_or_next_insert. 以$arr['index'] = 'cont'這個操作為例,PHP會嘗試先update相應的數據,如果沒有找到對應的Bucket,則表示這是一個新增的元素,因而會執行insert操作,這在_zend_hash_add_or_update中實現如下(省略非關鍵步驟):

ZEND_API int _zend_hash_add_or_update(HashTable *ht, const char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pD     est, int flag ZEND_FILE_LINE_DC)
{
    /* 由於是字符串索引,索引key不能為空,nKeyLength必須>0 */
	if (nKeyLength <= 0) {
         return FAILURE;
    }
	/* ht是否初始化,如果沒有,分配arBuckets的內存空間,設置nTableMask */
	CHECK_INIT(ht);
	
	/* 計算在hash表中的索引 */
	h = zend_inline_hash_func(arKey, nKeyLength);
	nIndex = h & ht->nTableMask;
	
	/* 掃描Bucket列表,看元素是否存在,如果存在,則更新之,並返回 */
       p = ht->arBuckets[nIndex];
	while (p != NULL) {
	  if (p->arKey == arKey ||
		((p->h == h) && (p->nKeyLength == nKeyLength) && !memcmp(p->arKey, arKey, nKeyLength))) {
		  /* 沖突,不能添加 */
		  if (flag & HASH_ADD) {
			return FAILURE;
		  }
		  HANDLE_BLOCK_INTERRUPTIONS();

		  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;
	}
	
	/* 不存在元素,則insert */
	if (IS_INTERNED(arKey)) {
        p = (Bucket *) pemalloc(sizeof(Bucket), ht->persistent);
        if (!p) {
            return FAILURE;
        }
        p->arKey = arKey;
    } else {
        p = (Bucket *) pemalloc(sizeof(Bucket) + nKeyLength, ht->persistent);
        if (!p) {
            return FAILURE;
        }
        p->arKey = (const char*)(p + 1);
        memcpy((char*)p->arKey, arKey, nKeyLength);
    }
    p->nKeyLength = nKeyLength;
    INIT_DATA(ht, p, pData, nDataSize);
    p->h = h;
	
  /* 插入到Buckets鏈表的頭部 */
    CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]);
	
  /* 插入到全局的雙鏈表,用於遍歷,是個邏輯隊列 */
  CONNECT_TO_GLOBAL_DLLIST(p, ht);
  ht->arBuckets[nIndex] = p;
	
  /* 增加元素個數 */
  ht->nNumOfElements++;
  /* 如果nNumOfElements > nTableSize,則需要對HashTable擴容 */
  ZEND_HASH_IF_FULL_DO_RESIZE(ht); 
}	

HashTable的更多操作如zend_hash_do_resize(擴容),zend_hash_rehash(擴容之後需要對原來hashTable的元素重新hash ),zend_hash_del_key_or_index(HashTable中刪除元素),zend_hash_destroy(銷毀Hash表),zend_hash_copy(hash表拷貝),這裡不再一一列舉,有興趣的同學可以翻看源碼查看。

四、相關參考資料:

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved