在PHP中,除了zval, 另一個比較重要的數據結構非hash table莫屬,例如我們最常見的數組,在底層便是hash table。除了數組,在線程安全(TSRM)、GC、資源管理、Global變量、ini配置管理中,幾乎都有Hash table的蹤跡(上一次我們也提到,符號表也是使用Hash table實現的)。那麼,在PHP中,這種數據有什麼特殊之處,結構是怎麼實現的? 帶着這些問題,我們開始本次的內核探索之旅。
本文主要內容:
- Hash table的基本介紹
- PHP底層Hash table的結構和實現
- Zend Hash table API
一、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++;
}
那爲什麼不直接用循環呢?
比如說:
1
2
3
|
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;
}
<br>/* 字符串索引的查找 */
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表拷貝),這裏不再一一列舉,有興趣的同學可以翻看源碼查看。
四、相關參考資料:
- http://blog.csdn.net/a600423444/article/details/8850617
- http://blog.csdn.net/hackooo/article/details/8949145
- http://blog.csdn.net/niujiaming0819/article/details/8568587
- http://blog.csdn.net/baiduforum/article/details/6644255 (推薦)
- http://www.phppan.com/2009/12/zend-hashtable/
- http://www.phppan.com/php-source-analytics/
- http://www.nowamagic.net/librarys/veda/detail/102/
- http://www.laruence.com/2009/07/23/994.html PHP中的hash算法
- http://www.cppblog.com/tx7do/archive/2011/07/26/151869.html DJBX33A
- http://nikic.github.io/2012/03/28/Understanding-PHPs-internal-array-implementation.html
- http://www.imsiren.com/archives/6
- http://blog.jobbole.com/11516/ 哈希表注入攻擊