轉載於http://www.phppan.com/2009/12/zend-hashtable/php
在PHP的Zend引擎中,有一個數據結構很是重要,它無處不在,是PHP數據存儲的核心,各類常量、變量、函數、類、對象等都用它來組織,這個數據結構就是HashTable。算法
HashTable在一般的數據結構教材中也稱做散列表或者哈希表。編程
哈希表的定義是:散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表。數組
哈希表基本原理比較簡單,但PHP的實現有其獨特的地方。理解了HashTable的數據存儲結構,對咱們分析PHP的源代碼,特別是Zend Engine中的虛擬機的實現時,有很重要的幫助。它能夠幫助咱們在大腦中模擬一個完整的虛擬機的形象。它也是PHP中其它一些數據結構如數組實現的基 礎。數據結構
Zend HashTable的實現結合了雙向鏈表和向量(數組)兩種數據結構的優勢,爲PHP提供了很是高效的數據存儲和查詢機制。app
Let’s begin!ide
1、 HashTable的數據結構函數
在Zend Engine中的HashTable的實現代碼主要包括zend_hash.h, zend_hash.c這兩個文件中。Zend HashTable包括兩個主要的數據結構,其一是Bucket(桶)結構,另外一個是HashTable結構。Bucket結構是用於保存數據的容器,而 HashTable結構則提供了對全部這些Bucket(或桶列)進行管理的機制。ui
typedef struct bucket { ulong h; /* Used for numeric indexing */ uint nKeyLength; /* key 長度 */ void *pData; /* 指向Bucket中保存的數據的指針 */ void *pDataPtr; /* 指針數據 */ struct bucket *pListNext; /* 指向HashTable桶列中下一個元素 */ struct bucket *pListLast; /* 指向HashTable桶列中前一個元素 */ struct bucket *pNext; /* 指向具備同一個hash值的桶列的後一個元素 */ struct bucket *pLast; /* 指向具備同一個hash值的桶列的前一個元素 */ char arKey[1]; /* 必須是最後一個成員,key名稱*/ } Bucket;
在Zend HashTable中,每一個數據元素(Bucket)有一個鍵名(key),它在整個HashTable中是惟一的,不能重複。根據鍵名能夠惟一肯定 HashTable中的數據元素。鍵名有兩種表示方式。第一種方式使用字符串arKey做爲鍵名,該字符串的長度爲nKeyLength。注意到在上面的 數據結構中arKey雖然只是一個長度爲1的字符數組,但它並不意味着key只能是一個字符。實際上Bucket是一個可變長的結構體,因爲arKey是 Bucket的最後一個成員變量,經過arKey與nKeyLength結合可肯定一個長度爲nKeyLength的key。這是C語言編程中的一個比較 經常使用的技巧。另外一種鍵名的表示方式是索引方式,這時nKeyLength老是0,長整型字段h就表示該數據元素的鍵名。簡單的來講,即若是 nKeyLength=0,則鍵名爲h;不然鍵名爲arKey, 鍵名的長度爲nKeyLength。spa
當nKeyLength > 0時,並不表示這時的h值就沒有意義。事實上,此時它保存的是arKey對應的hash值。無論hash函數怎麼設計,衝突都是不可避免的,也就是說不一樣 的arKey可能有相同的hash值。具備相同hash值的Bucket保存在HashTable的arBuckets數組(參考下面的解釋)的同一個索 引對應的桶列中。這個桶列是一個雙向鏈表,其前向元素,後向元素分別用pLast, pNext來表示。新插入的Bucket放在該桶列的最前面。
在Bucket中,實際的數據是保存在pData指針指向的內存塊中,一般這個內存塊是系統另外分配的。但有一種狀況例外,就是當Bucket保存 的數據是一個指針時,HashTable將不會另外請求系統分配空間來保存這個指針,而是直接將該指針保存到pDataPtr中,而後再將pData指向 本結構成員的地址。這樣能夠提升效率,減小內存碎片。由此咱們能夠看到PHP HashTable設計的精妙之處。若是Bucket中的數據不是一個指針,pDataPtr爲NULL。
HashTable中全部的Bucket經過pListNext, pListLast構成了一個雙向鏈表。最新插入的Bucket放在這個雙向鏈表的最後。
注意在通常狀況下,Bucket並不能提供它所存儲的數據大小的信息。因此在PHP的實現中,Bucket中保存的數據必須具備管理自身大小的能力。
typedef struct _hashtable { uint nTableSize; uint nTableMask; uint nNumOfElements; ulong nNextFreeElement; Bucket *pInternalPointer; 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;
在HashTable結構中,nTableSize指定了HashTable的大小,同時它限定了HashTable中能保存Bucket的最大數量,此 數越大,系統爲HashTable分配的內存就越多。爲了提升計算效率,系統自動會將nTableSize調整到最小一個不小於nTableSize的2 的整數次方。也就是說,若是在初始化HashTable時指定一個nTableSize不是2的整數次方,系統將會自動調整nTableSize的值。即
nTableSize = 2ceil(log(nTableSize, 2)) 或 nTableSize = pow(ceil(log(nTableSize,2)))
例如,若是在初始化HashTable的時候指定nTableSize = 11,HashTable初始化程序會自動將nTableSize增大到16。
arBuckets是HashTable的關鍵,HashTable初始化程序會自動申請一塊內存,並將其地址賦值給arBuckets,該內存大 小正好能容納nTableSize個指針。咱們能夠將arBuckets看做一個大小爲nTableSize的數組,每一個數組元素都是一個指針,用於指向 實際存放數據的Bucket。固然剛開始時每一個指針均爲NULL。
nTableMask的值永遠是nTableSize – 1,引入這個字段的主要目的是爲了提升計算效率,是爲了快速計算Bucket鍵名在arBuckets數組中的索引。
nNumberOfElements記錄了HashTable當前保存的數據元素的個數。當nNumberOfElement大於nTableSize時,HashTable將自動擴展爲原來的兩倍大小。
nNextFreeElement記錄HashTable中下一個可用於插入數據元素的arBuckets的索引。
pListHead, pListTail則分別表示Bucket雙向鏈表的第一個和最後一個元素,這些數據元素一般是根據插入的順序排列的。也能夠經過各類排序函數對其進行重 新排列。pInternalPointer則用於在遍歷HashTable時記錄當前遍歷的位置,它是一個指針,指向當前遍歷到的Bucket,初始值是 pListHead。
pDestructor是一個函數指針,在HashTable的增長、修改、刪除Bucket時自動調用,用於處理相關數據的清理工做。
persistent標誌位指出了Bucket內存分配的方式。若是persisient爲TRUE,則使用操做系統自己的內存分配函數爲Bucket分配內存,不然使用PHP的內存分配函數。具體請參考PHP的內存管理。
nApplyCount與bApplyProtection結合提供了一個防止在遍歷HashTable時進入遞歸循環時的一種機制。
inconsistent成員用於調試目的,只在PHP編譯成調試版本時有效。表示HashTable的狀態,狀態有四種:
狀態值 含義
HT_IS_DESTROYING 正在刪除全部的內容,包括arBuckets自己
HT_IS_DESTROYED 已刪除,包括arBuckets自己
HT_CLEANING 正在清除全部的arBuckets指向的內容,但不包括arBuckets自己
HT_OK 正常狀態,各類數據徹底一致
typedef struct _zend_hash_key { char *arKey; /* hash元素key名稱 */ uint nKeyLength; /* hash 元素key長度 */ ulong h; /* key計算出的hash值或直接指定的數值下標 */ } zend_hash_key;
如今來看zend_hash_key結構就比較容易理解了。它經過arKey, nKeyLength, h三個字段惟一肯定了HashTable中的一個元素。
根據上面對HashTable相關數據結構的解釋,咱們能夠畫出HashTable的內存結構圖:
2、 Zend HashTable的實現
本節具體介紹一下PHP中HashTable的實現。如下函數均取自於zend_hash.c。只要充分理解了上述數據結構,HashTable實現的代碼並不難理解。
1 HashTable初始化
HashTable提供了一個zend_hash_init宏來完成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; Bucket **tmp; SET_INCONSISTENT(HT_OK); if (nSize >= 0×80000000) { /* prevent overflow */ ht->nTableSize = 0×80000000; } else { while ((1U << i) < nSize) { /* 自動調整nTableSize至2的n次方 */ i++; } ht->nTableSize = 1 << i; /* i的最小值爲3,所以HashTable大小最小爲8 */ } 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; /* 根據persistent使用不一樣方式分配arBuckets內存,並將其全部指針初始化爲NULL*/ /* 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; }
在之前的版本中,可使用pHashFunction來指定hash函數。但現PHP已強制使用DJBX33A算法,所以實際上pHashFunction這個參數並不會用到,保留在這裏只是爲了與之前的代碼兼容。
2 增長、插入和修改元素
向HashTable中添加一個新的元素最關鍵的就是要肯定將這個元素插入到arBuckets數組中的哪一個位置。根據上面對Bucket結構鍵名 的解釋,咱們能夠知道有兩種方式向HashTable添加一個新的元素。第一種方法是使用字符串做爲鍵名來插入Bucket;第二種方法是使用索引做爲鍵 名來插入Bucket。第二種方法具體又能夠分爲兩種狀況:指定索引或不指定索引,指定索引指的是強制將Bucket插入到指定的索引位置中;不指定索引 則將Bucket插入到nNextFreeElement對應的索引位置中。這幾種插入數據的方法實現比較相似,不一樣的只是定位Bucket的方法。
修改HashTable中的數據的方法與增長數據的方法也很相似。
咱們先看第一種使用字符串做爲鍵名增長或修改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函數計算arKey的hash值 */ h = zend_inline_hash_func(arKey, nKeyLength); /* 將hash值和nTableMask按位與後生成該元素在arBuckets中的索引。讓它和 * nTableMask按位與是保證不會產生一個使得arBuckets越界的數組下標。 */ nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; /* 取得相應索引對應的Bucket的指針 */ /* 檢查對應的桶列中是否包含有數據元素(key, 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; } /* HashTable中沒有key對應的數據,新增一個Bucket */ 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; // 將Bucket加入到相應的桶列中 CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]); if (pDest) { *pDest = p->pData; } HANDLE_BLOCK_INTERRUPTIONS(); // 將Bucket 加入到HashTable的雙向鏈表中 CONNECT_TO_GLOBAL_DLLIST(p, ht); ht->arBuckets[nIndex] = p; HANDLE_UNBLOCK_INTERRUPTIONS(); ht->nNumOfElements++; // 若是HashTable已滿,從新調整HashTable的大小。 ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* If the Hash table is full, resize it */ return SUCCESS; }
由於這個函數是使用字符串做爲鍵名來插入數據的,所以它首先檢查nKeyLength的值是否大於0,若是不是的話就直接退出。而後計算arKey對應的 hash值h,將其與nTableMask按位與後獲得一個無符號整數nIndex。這個nIndex就是將要插入的Bucket在arBuckets數 組中的索引位置。
如今已經有了arBuckets數組的一個索引,咱們知道它包括的數據是一個指向Bucket的雙向鏈表的指針。若是這個雙向鏈表不爲空的話咱們首先檢查 這個雙向鏈表中是否已經包含了用字符串arKey指定的鍵名的Bucket,這樣的Bucket若是存在,而且咱們要作的操做是插入新Bucket(經過 flag標識),這時就應該報錯 – 由於在HashTable中鍵名不能夠重複。若是存在,而且是修改操做,則使用在HashTable中指定了析構函數pDestructor對原來的 pData指向的數據進行析構操做;而後將用新的數據替換原來的數據便可成功返回修改操做。
若是在HashTable中沒有找到鍵名指定的數據,就將該數據封裝到Bucket中,而後插入HashTable。這裏要注意的是以下的兩個宏:
CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex])
CONNECT_TO_GLOBAL_DLLIST(p, ht)
前者是將該Bucket插入到指定索引的Bucket雙向鏈表中,後者是插入到整個HashTable的Bucket雙向鏈表中。二者的插入方式也不一樣,前者是將該Bucket插入到雙向鏈表的最前面,後者是插入到雙向鏈表的最末端。
下面是第二種插入或修改Bucket的方法,即便用索引的方法:
ZEND_API int _zend_hash_index_update_or_next_insert(HashTable *ht, ulong h, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC) { uint nIndex; Bucket *p; IS_CONSISTENT(ht); if (flag & HASH_NEXT_INSERT) { h = ht->nNextFreeElement; } nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; // 檢查是否含有相應的數據 while (p != NULL) { if ((p->nKeyLength == 0) && (p->h == h)) { if (flag & HASH_NEXT_INSERT || flag & HASH_ADD) { return FAILURE; } // // …… 修改Bucket數據,略 // if ((long)h >= (long)ht->nNextFreeElement) { ht->nNextFreeElement = h + 1; } if (pDest) { *pDest = p->pData; } return SUCCESS; } p = p->pNext; } p = (Bucket *) pemalloc_rel(sizeof(Bucket) - 1, ht->persistent); if (!p) { return FAILURE; } p->nKeyLength = 0; /* Numeric indices are marked by making the nKeyLength == 0 */ p->h = h; INIT_DATA(ht, p, pData, nDataSize); if (pDest) { *pDest = p->pData; } CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]); HANDLE_BLOCK_INTERRUPTIONS(); ht->arBuckets[nIndex] = p; CONNECT_TO_GLOBAL_DLLIST(p, ht); HANDLE_UNBLOCK_INTERRUPTIONS(); if ((long)h >= (long)ht->nNextFreeElement) { ht->nNextFreeElement = h + 1; } ht->nNumOfElements++; ZEND_HASH_IF_FULL_DO_RESIZE(ht); return SUCCESS; }
flag標誌指明當前操做是HASH_NEXT_INSERT(不指定索引插入或修改), HASH_ADD(指定索引插入)仍是HASH_UPDATE(指定索引修改)。因爲這些操做的實現代碼基本相同,所以統一合併成了一個函數,再用flag加以區分。
本函數基本與前一個相同,不一樣的是若是肯定插入到arBuckets數組中的索引的方法。若是操做是HASH_NEXT_INSERT,則直接使用nNextFreeElement做爲插入的索引。注意nNextFreeElement的值是如何使用和更新的。
3 訪問元素
一樣,HashTable用兩種方式來訪問元素,一種是使用字符串arKey的zend_hash_find();另外一種是使用索引的訪問方式zend_hash_index_find()。因爲其實現的代碼很簡單,分析工做就留給讀者自已完成。
4 刪除元素
HashTable刪除數據均使用zend_hash_del_key_or_index()函數來完成,其代碼也較爲簡單,這裏也再也不詳細分析。須要的是注意如何根據arKey或h來計算出相應的下標,以及兩個雙向鏈表的指針的處理。
5 遍歷元素
/* This is used to recurse elements and selectively delete certain entries * from a hashtable. apply_func() receives the data and decides if the entry * should be deleted or recursion should be stopped. The following three * return codes are possible: * ZEND_HASH_APPLY_KEEP - continue * ZEND_HASH_APPLY_STOP - stop iteration * ZEND_HASH_APPLY_REMOVE - delete the element, combineable with the former */ ZEND_API void zend_hash_apply(HashTable *ht, apply_func_t apply_func TSRMLS_DC) { Bucket *p; IS_CONSISTENT(ht); HASH_PROTECT_RECURSION(ht); p = ht->pListHead; while (p != NULL) { int result = apply_func(p->pData TSRMLS_CC); if (result & ZEND_HASH_APPLY_REMOVE) { p = zend_hash_apply_deleter(ht, p); } else { p = p->pListNext; } if (result & ZEND_HASH_APPLY_STOP) { break; } } HASH_UNPROTECT_RECURSION(ht); }
由於HashTable中全部Bucket均可以經過pListHead指向的雙向鏈表來訪問,所以遍歷HashTable的實現也比較簡單。這裏值得一 提的是對當前遍歷到的Bucket的處理使用了一個apply_func_t類型的回調函數。根據實際須要,該回調函數返回下面值之一:
ZEND_HASH_APPLY_KEEP
ZEND_HASH_APPLY_STOP
ZEND_HASH_APPLY_REMOVE
它們分別表示繼續遍歷,中止遍歷或刪除相應元素後繼續遍歷。
還有一個要注意的問題就是遍歷時的防止遞歸的問題,也就是防止對同一個HashTable同時進行屢次遍歷。這是用下面兩個宏來實現的:
HASH_PROTECT_RECURSION(ht)
HASH_UNPROTECT_RECURSION(ht)
其主要原理是若是遍歷保護標誌bApplyProtection爲真,則每次進入遍歷函數時將nApplyCount值加1,退出遍歷函數時將nApplyCount值減1。開始遍歷以前若是發現nApplyCount > 3就直接報告錯誤信息並退出遍歷。
上面的apply_func_t不帶參數。HashTable還提供帶一個參數或可變參數的回調方式,對應的遍歷函數分別爲:
typedef int (*apply_func_arg_t)(void *pDest,void *argument TSRMLS_DC); void zend_hash_apply_with_argument(HashTable *ht, apply_func_arg_t apply_func, void *data TSRMLS_DC); typedef int (*apply_func_args_t)(void *pDest, int num_args, va_list args, zend_hash_key *hash_key); void zend_hash_apply_with_arguments(HashTable *ht, apply_func_args_t apply_func, int numargs, …);
除了上面提供的幾種提供外,還有許多其它操做HashTable的API。如排序、HashTable的拷貝與合併等等。只要充分理解了上述HashTable的數據結構,理解這些代碼並不困難。