PHP核心中的神器之HashTable

來源:互聯網
上載者:User
一、雜湊表定義

雜湊表(或散列表),是將鍵名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結構:
typedef struct _hashtable {     uint nTableSize; //表長度,並非元素個數     uint nTableMask;//表的掩碼,始終等於nTableSize-1     uint 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;


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演算法的複雜度和提高效能。


1.1)在PHP中初始化一個空數組時,對應核心中是如何建立HashTable的
$array = new Array();
//省略了部分代碼,提出主要的邏輯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=8          while ((1U << i) < nSize) {               i++;          }          ht->nTableSize = 1 << i;     }     ht->nTableMask = ht->nTableSize - 1;     ....         return SUCCESS;}
從上看出,即使在PHP中初始化一個空數組或不足8個元素的數組,都會被建立8個長度的HashTable。同樣建立100個元素的數組,也會被分配128長度的HashTable。依次類推。

1.2)核心對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。


1.3) 核心對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;}


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的的雙向鏈表。

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

之後,將會寫一篇關於利用Hash演算法,進行分布式儲存的介紹。

原文地址:http://blog.csdn.net/a600423444/article/details/8850617

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.