1. 程式人生 > >PHP核心中的神器之HashTable

PHP核心中的神器之HashTable

轉載:https://blog.csdn.net/a600423444/article/details/8850617

一、雜湊表定義

雜湊表(或散列表),是將鍵名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方法後,實際上使用的是雙向連結串列


下面通過原始碼來一步一步分析。

1)HashTable在PHP核心的實現

PHP實現HashTable主要是通過兩個資料結構Bucket(桶)和HashTable。
從PHP指令碼端來看,HashTable相當於Array物件,而Bucket相當於Array物件裡的某個元素。對於多維陣列實際就是HashTable的某個Bucket裡儲存著另一個HashTable。
HashTable結構:

      
  1. typedef struct _hashtable {
  2. uint nTableSize; //表長度,並非元素個數
  3. uint nTableMask; //表的掩碼,始終等於nTableSize-1
  4. uint nNumOfElements; //儲存的元素個數
  5. ulong nNextFreeElement; //指向下一個空的元素位置
  6. Bucket *pInternalPointer; //foreach迴圈時,用來記錄當前遍歷到的元素位置
  7. Bucket *pListHead;
  8. Bucket *pListTail;
  9. Bucket **arBuckets; //儲存的元素陣列
  10. dtor_func_t pDestructor; //解構函式
  11. zend_bool persistent; //是否持久儲存。從這可以發現,PHP陣列是可以實現持久儲存在記憶體中的,而無需每次請求都重新載入。
  12. unsigned char nApplyCount;
  13. zend_bool bApplyProtection;
  14. } HashTable;

Bucket結構:

      
  1. typedef struct bucket {
  2. ulong h; //陣列索引
  3. uint nKeyLength; //字串索引的長度
  4. void *pData; //實際資料的儲存地址
  5. void *pDataPtr; //引入的資料儲存地址
  6. struct bucket *pListNext;
  7. struct bucket *pListLast;
  8. struct bucket *pNext; //雙向連結串列的下一個元素的地址
  9. struct bucket *pLast; //雙向連結串列的下一個元素地址
  10. char arKey[ 1]; /* Must be last element */
  11. } Bucket;

PHP核心雜湊表的雜湊函式很簡單,直接使用 (HashTable->nTableSize & HashTable->nTableMask)的結果作為雜湊函式的實現。這樣做的目的可能也是為了降低Hash演算法的複雜度和提高效能
1.1)在PHP中初始化一個空陣列時,對應核心中是如何建立HashTable的
$array = new Array();
      

      
  1. //省略了部分程式碼,提出主要的邏輯
  2. 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)
  3. {
  4. uint i = 3;
  5. Bucket **tmp;
  6. SET_INCONSISTENT(HT_OK);
  7. if (nSize >= 0x80000000) { //陣列的最大長度是十進位制2147483648
  8. /* prevent overflow */
  9. ht->nTableSize = 0x80000000;
  10. } else {
  11. //陣列的長度是向2的整次冪取圓整
  12. //例如陣列的裡有10個元素,那麼實際被分配的HashTable長度是16。100個元素,則被分配128的長度
  13. //HashTable的最小長度是8,而非0。因為預設是將1向右移3位,1<<3=8
  14. while (( 1U << i) < nSize) {
  15. i++;
  16. }
  17. ht->nTableSize = 1 << i;
  18. }
  19. ht->nTableMask = ht->nTableSize - 1;
  20. ....
  21. return SUCCESS;
  22. }
從上看出,即使在PHP中初始化一個空陣列或不足8個元素的陣列,都會被建立8個長度的HashTable。同樣建立100個元素的陣列,也會被分配128長度的HashTable。依次類推。

1.2)核心對PHP新增數字索引的處理方式

PHP陣列中,鍵名可以為數字或字串型別。而在核心中只允許數字索引,對於字串索引,核心採用了time33演算法將字串轉換為整型。具體的實現下面會詳細說明。
$array[0] = "hello hashtable";
      

      
  1. //省略了部分程式碼,提出主要的邏輯
  2. 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)
  3. {
  4. ulong h;
  5. uint nIndex;
  6. Bucket *p;
  7. //省略了部分程式碼,提出主要的邏輯
  8. nIndex = h & ht->nTableMask;
  9. p = ht->arBuckets[nIndex];
  10. p = (Bucket *) pemalloc_rel( sizeof(Bucket) - 1, ht->persistent);
  11. if (!p) {
  12. return FAILURE;
  13. }
  14. p->nKeyLength = 0; /* Numeric indices are marked by making the nKeyLength == 0 */
  15. p->h = h;
  16. INIT_DATA(ht, p, pData, nDataSize);
  17. if (pDest) {
  18. *pDest = p->pData;
  19. }
  20. ht->arBuckets[nIndex] = p;
  21. ht->nNumOfElements++;
  22. return SUCCESS;
  23. }


上述也說明了,核心中雜湊表的雜湊函式就是簡單的h & ht->nTableMask,其中h代表PHP中設定的索引號,nTableMask等於雜湊表分配的長度-1。

1.3) 核心對PHP中字串索引的處理方式

$array['index'] = "hello hashtable";
      

與數字索引相比,只是多了一步將字串轉換為整型。用到的演算法是time33
下面貼出了演算法的實現,就是對字串的每個字元轉換為ASCII碼乘上33並且相加得到的結果。

      
  1. static inline ulong zend_inline_hash_func(const char *arKey, uint nKeyLength)
  2. {
  3. register ulong hash = 5381;
  4. /* variant with the hash unrolled eight times */
  5. for (; nKeyLength >= 8; nKeyLength -= 8) {
  6. hash = ((hash << 5) + hash) + *arKey++;
  7. hash = ((hash << 5) + hash) + *arKey++;
  8. hash = ((hash << 5) + hash) + *arKey++;
  9. hash = ((hash << 5) + hash) + *arKey++;
  10. hash = ((hash << 5) + hash) + *arKey++;
  11. hash = ((hash << 5) + hash) + *arKey++;
  12. hash = ((hash << 5) + hash) + *arKey++;
  13. hash = ((hash << 5) + hash) + *arKey++;
  14. }
  15. switch (nKeyLength) {
  16. case 7: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
  17. case 6: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
  18. case 5: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
  19. case 4: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
  20. case 3: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
  21. case 2: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
  22. case 1: hash = ((hash << 5) + hash) + *arKey++; break;
  23. case 0: break;
  24. }
  25. return hash;
  26. }
  27. zend_hash.c
  28. //下面省略了部分程式碼,提出主要的邏輯
  29. 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)
  30. {
  31. ulong h;
  32. uint nIndex;
  33. Bucket *p;
  34. h = zend_inline_hash_func(arKey, nKeyLength); //字串轉整型
  35. nIndex = h & ht->nTableMask;
  36. p = ht->arBuckets[nIndex];
  37. p = (Bucket *) pemalloc_rel( sizeof(Bucket) - 1, ht->persistent);
  38. if (!p) {
  39. return FAILURE;
  40. }
  41. p->nKeyLength = 0; /* Numeric indices are marked by making the nKeyLength == 0 */
  42. p->h = h;
  43. INIT_DATA(ht, p, pData, nDataSize);
  44. if (pDest) {
  45. *pDest = p->pData;
  46. }
  47. ht->arBuckets[nIndex] = p;
  48. ht->nNumOfElements++;
  49. return SUCCESS;
  50. }

2) 核心中如何實現均勻分佈和解決hash碰撞問題的

2.1) 均勻分佈
均勻分佈是指,將需要儲存的各個元素均勻的分佈到HashTable中。
而負責計算具體分佈到表中哪個位置的函式就是雜湊函式做的事情,所以雜湊函式的實現直接關係到均勻分佈的效率。
上面也提到了PHP核心中用了簡單的方式實現:h & ht->nTableMask;

2.1)Hash碰撞

Hash碰撞是指,經過Hash演算法後得到的值會出現key1 != key2, 但Hash(key1)卻等於Hash(key2)的情況,這就是碰撞問題。
在PHP核心來看,就是會出現key1 != key2, 但key1 & ht->nTableMask卻等於 key2 & ht->nTableMask的情況。
PHP核心使用雙向連結串列的方式來儲存衝突的資料。即Bucket本身也是一個雙向連結串列,當發生衝突時,會將資料按順序向後排列。
如果不發生衝突,Bucket即是長度為1的的雙向連結串列。

      
  1. ZEND_API int zend_hash_find(const HashTable *ht, const char *arKey, uint nKeyLength, void **pData)
  2. {
  3. ulong h;
  4. uint nIndex;
  5. Bucket *p;
  6. IS_CONSISTENT(ht);
  7. h = zend_inline_hash_func(arKey, nKeyLength);
  8. nIndex = h & ht->nTableMask;
  9. p = ht->arBuckets[nIndex];
  10. //找到元素時,並非立即返回,而是要再對比h與nKeyLength,防止hash碰撞。此段程式碼就是遍歷連結串列,直到連結串列尾部。
  11. while (p != NULL) {
  12. if ((p->h == h) && (p->nKeyLength == nKeyLength)) {
  13. if (! memcmp(p->arKey, arKey, nKeyLength)) {
  14. *pData = p->pData;
  15. return SUCCESS;
  16. }
  17. }
  18. p = p->pNext;
  19. }
  20. return FAILURE;
  21. }



之後,將會寫一篇關於利用Hash演算法,進行分散式儲存的介紹。 原文地址: http://blog.csdn.net/a600423444/article/details/8850617

轉載:https://blog.csdn.net/a600423444/article/details/8850617

一、雜湊表定義

雜湊表(或散列表),是將鍵名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方法後,實際上使用的是雙向連結串列


下面通過原始碼來一步一步分析。

1)HashTable在PHP核心的實現

PHP實現HashTable主要是通過兩個資料結構Bucket(桶)和HashTable。
從PHP指令碼端來看,HashTable相當於Array物件,而Bucket相當於Array物件裡的某個元素。對於多維陣列實際就是HashTable的某個Bucket裡儲存著另一個HashTable。
HashTable結構:

    
  1. typedef struct _hashtable {
  2. uint nTableSize; //表長度,並非元素個數
  3. uint nTableMask; //表的掩碼,始終等於nTableSize-1
  4. uint nNumOfElements; //儲存的元素個數
  5. ulong nNextFreeElement; //指向下一個空的元素位置
  6. Bucket *pInternalPointer; //foreach迴圈時,用來記錄當前遍歷到的元素位置
  7. Bucket *pListHead;
  8. Bucket *pListTail;
  9. Bucket **arBuckets; //儲存的元素陣列
  10. dtor_func_t pDestructor; //解構函式
  11. zend_bool persistent; //是否持久儲存。從這可以發現,PHP陣列是可以實現持久儲存在記憶體中的,而無需每次請求都重新載入。
  12. unsigned char nApplyCount;
  13. zend_bool bApplyProtection;
  14. } HashTable;

Bucket結構: