PHP Hashtable实现源码分析
轉載:http://www.qixing318.com/article/php-hashtable-implementation-source-code-analysis.html
一、哈希表定義
哈希表(或散列表),是將鍵名key按指定的散列函數HASH經過HASH(key)計算后映射到表中一個記錄,而這個數組就是哈希表。
這里的HASH指任意的函數,例如MD5、CRC32、SHA1或你自定義的函數實現。
二、HashTable性能
HashTable是一種查找性能極高的數據結構,在很多語言內部都實現了HashTable。
理想情況下HashTable的性能是O(1)的,性能消耗主要集中在散列函數HASH(key),通過HASH(key)直接定位到表中的記錄。
而在實際情況下經常會發生key1 != key2,但HASH(key1) = HASH(key2),這種情況即Hash碰撞問題,碰撞的概率越低HashTable的性能越好。當然Hash算法太過復雜也會影響HashTable性能。
三、理解PHP的哈希表實現
在PHP內核也同樣實現了HashTable并廣泛應用,包括線程安全、全局變量、資源管理等基本上所有的地方都能看到它的身影。
不僅如此,在PHP腳本中數組(PHP的數組實質就是HashTable)也是被廣泛使用的,例如數組形式的配置文件、數據庫的查詢結果等,可以說是無處不在。
那么既然PHP的數組使用率這么高,內部是如何實現的?它如何解決hash碰撞及實現均勻分布的?PHP腳本使用數組應該注意哪些?
首先通過圖解,大致理解PHP HashTable的實現。
修正:之前認為PHP解決Hahs沖突時,鏈表使用的是單向鏈表,查看\Zend\zend_hash.c的zend_hash_move_backwards_ex方法與zend_hash_del_key_or_index方法后,實際上使用的是雙向鏈表。
<img src="http://www.qixing318.com/static/p_w_picpaths/lazy.gif" alt="PHP HashTable的實現" title="PHP HashTable的實現" title"="" style="border: 1px solid rgb(230, 230, 230); margin: 0px auto; max-width: 720px; padding: 5px; text-align: center; display: block;">
下面通過源碼來一步一步分析。
四、HashTable在PHP內核的實現
PHP實現HashTable主要是通過兩個數據結構Bucket(桶)和HashTable。
從PHP腳本端來看,HashTable相當于Array對象,而Bucket相當于Array對象里的某個元素,對于多維數組實際就是HashTable的某個Bucket里存儲著另一個HashTable。
1. HashTable結構
typedef?struct?_hashtable?{uint?nTableSize;?//表長度,并非元素個數uint?nTableMask;//表的掩碼,始終等于nTableSize-1uint?nNumOfElements;//存儲的元素個數ulong?nNextFreeElement;//指向下一個空的元素位置Bucket?*pInternalPointer;//foreach循環時,用來記錄當前遍歷到的元素位置Bucket?*pListHead;Bucket?*pListTail;Bucket?**arBuckets;//存儲的元素數組dtor_func_t?pDestructor;//析構函數zend_bool?persistent;//是否持久保存。從這可以發現,PHP數組是可以實現持久保存在內存中的,而無需每次請求都重新加載。unsigned?char?nApplyCount;zend_bool?bApplyProtection; }?HashTable;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;//雙向鏈表的下一個元素地址char?arKey[1];?/*?Must?be?last?element?*/ }?Bucket;PHP內核哈希表的散列函數很簡單,直接使用 (HashTable->nTableSize & HashTable->nTableMask)的結果作為散列函數的實現。這樣做的目的可能也是為了降低Hash算法的復雜度和提高性能。
在PHP中初始化一個空數組時,對應內核中是如何創建HashTable的。
$array?=?new?Array();PHP內核創建對應的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?>=?0x80000000)?{//數組的最大長度是十進制2147483648/*?prevent?overflow?*/ht->nTableSize?=?0x80000000;}?else?{//數組的長度是向2的整次冪取圓整//例如數組的里有10個元素,那么實際被分配的HashTable長度是16。100個元素,則被分配128的長度//HashTable的最小長度是8,而非0。因為默認是將1向右移3位,1<<3=8while?((1U?<<?i)?<?nSize)?{i++;}ht->nTableSize?=?1?<<?i;}ht->nTableMask?=?ht->nTableSize?-?1;....return?SUCCESS; }從上看出,即使在PHP中初始化一個空數組或不足8個元素的數組,都會被創建8個長度的HashTable。同樣創建100個元素的數組,也會被分配128長度的HashTable,依次類推。
3. 內核對PHP添加數字索引的處理方式
PHP數組中,鍵名可以為數字或字符串類型。而在內核中只允許數字索引,對于字符串索引,內核采用了time33算法將字符串轉換為整型。具體的實現下面會詳細說明。
$array[0]?=?"hello?hashtable";//省略了部分代碼,提出主要的邏輯 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) {????ulong?h;uint?nIndex;Bucket?*p;//省略了部分代碼,提出主要的邏輯nIndex?=?h?&?ht->nTableMask;p?=?ht->arBuckets[nIndex];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;}ht->arBuckets[nIndex]?=?p;ht->nNumOfElements++;return?SUCCESS; }上述也說明了,內核中哈希表的散列函數就是簡單的h & ht->nTableMask,其中h代表PHP中設置的索引號,nTableMask等于哈希表分配的長度-1。
4. 內核對PHP中字符串索引的處理方式
$array['index']?=?"hello?hashtable";與數字索引相比,只是多了一步將字符串轉換為整型。用到的算法是time33,下面貼出了算法的實現,就是對字符串的每個字符轉換為ASCII碼乘上33并且相加得到的結果。
static?inline?ulong?zend_inline_hash_func(const?char?*arKey,?uint?nKeyLength) {register?ulong?hash?=?5381;/*?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;}return?hash; }zend_hash.c //下面省略了部分代碼,提出主要的邏輯 ZEND_API?int?_zend_hash_add_or_update(HashTable?*ht,?const?char?*arKey,?uint?nKeyLength,?void?*pData,?uint?nDataSize,?void?**pDest,?int?flag?ZEND_FILE_LINE_DC) {?????ulong?h;uint?nIndex;Bucket?*p;h?=?zend_inline_hash_func(arKey,?nKeyLength);?//字符串轉整型nIndex?=?h?&?ht->nTableMask;p?=?ht->arBuckets[nIndex];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;}ht->arBuckets[nIndex]?=?p;ht->nNumOfElements++;return?SUCCESS; }五、內核中如何實現均勻分布和解決hash碰撞問題的
1. 均勻分布
均勻分布是指,將需要存儲的各個元素均勻的分布到HashTable中。
而負責計算具體分布到表中哪個位置的函數就是散列函數做的事情,所以散列函數的實現直接關系到均勻分布的效率。
上面也提到了PHP內核中用了簡單的方式實現:h & ht->nTableMask;
2. Hash碰撞
Hash碰撞是指,經過Hash算法后得到的值會出現key1 != key2, 但Hash(key1)卻等于Hash(key2)的情況,這就是碰撞問題。
在PHP內核來看,就是會出現key1 != key2, 但key1 & ht->nTableMask卻等于key2 & ht->nTableMask的情況。
PHP內核使用雙向鏈表的方式來存儲沖突的數據。即Bucket本身也是一個雙向鏈表,當發生沖突時,會將數據按順序向后排列。
如果不發生沖突,Bucket即是長度為1的的雙向鏈表。
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);h?=?zend_inline_hash_func(arKey,?nKeyLength);nIndex?=?h?&?ht->nTableMask;p?=?ht->arBuckets[nIndex];//找到元素時,并非立即返回,而是要再對比h與nKeyLength,防止hash碰撞。此段代碼就是遍歷鏈表,直到鏈表尾部。while?(p?!=?NULL)?{if?((p->h?==?h)?&&?(p->nKeyLength?==?nKeyLength))?{if?(!memcmp(p->arKey,?arKey,?nKeyLength))?{*pData?=?p->pData;return?SUCCESS;}}p?=?p->pNext;}return?FAILURE; }轉載于:https://blog.51cto.com/mengphilip/1630439
總結
以上是生活随笔為你收集整理的PHP Hashtable实现源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 机器学习之分类模型
- 下一篇: php连接oracle