http://www.nowamagic.net/academy/detail/1201006
- Chapter: PHP中的Hash算法
-
- 1. 从PHP的Hash(哈希)算法开始
- 2. Zend 哈希表的内部实现
- 3. PHP哈希表结构的深入剖析
数据结构
PHP中使用一个叫Bucket的结构体表示桶(桶的相关参考Linux内核中的hash与bucket
),同一哈希值的所有桶被组织为一个单链表。哈希表使用HashTable结构体表示。相关源码在zend/Zend_hash.h下:
01 | typedef struct bucket { |
02 | /* Used for numeric indexing */ |
03 | ulong h; // 对char *key进行hash后的值,或者是用户指定的数字索引值 |
04 | uint nKeyLength; // hash关键字的长度,如果数组索引为数字,此值为0 |
05 | void *pData; // 指向value,一般是用户数据的副本,如果是指针数据,则指向pDataPtr |
06 | void *pDataPtr; //如果是指针数据,此值会指向真正的value,同时上面pData会指向此值 |
07 | struct bucket *pListNext; // 整个hash表的下一元素 |
08 | struct bucket *pListLast; // 整个哈希表该元素的上一个元素 |
09 | struct bucket *pNext; // 存放在同一个hash Bucket内的下一个元素 |
10 | struct bucket *pLast; // 同一个哈希bucket的上一个元素 |
11 | char arKey[1]; |
12 | /*存储字符索引,此项必须放在最未尾,因为此处只字义了1个字节,存储的实际上是指向char *key的值, |
13 | 这就意味着可以省去再赋值一次的消耗,而且,有时此值并不需要,所以同时还节省了空间。 |
14 | */ |
15 | } Bucket; |
16 |
17 | typedef struct _hashtable { |
18 | uint nTableSize; // hash Bucket的大小,最小为8,以2x增长。 |
19 | uint nTableMask; // nTableSize-1 , 索引取值的优化 |
20 | uint nNumOfElements; // hash Bucket中当前存在的元素个数,count()函数会直接返回此值 |
21 | ulong nNextFreeElement; // 下一个数字索引的位置 |
22 | Bucket *pInternalPointer; // 当前遍历的指针(foreach比for快的原因之一) |
23 | Bucket *pListHead; // 存储数组头元素指针 |
24 | Bucket *pListTail; // 存储数组尾元素指针 |
25 | Bucket **arBuckets; // 存储hash数组 |
26 | dtor_func_t pDestructor; |
27 | zend_bool persistent; |
28 | unsigned char nApplyCount; // 标记当前hash Bucket被递归访问的次数(防止多次递归) |
29 | zend_bool bApplyProtection; // 标记当前hash桶允许不允许多次访问,不允许时,最多只能递归3次 |
30 | #if ZEND_DEBUG |
31 | int inconsistent; |
32 | #endif |
33 | } HashTable; |
重点明确下面几个字段:
- Bucket中的“h”用于存储原始key;
- HashTable中的nTableMask是一个掩码,一般被设为nTableSize - 1,与哈希算法有密切关系,后面讨论哈希算法时会详述;
- arBuckets指向一个指针数组,其中每个元素是一个指向Bucket链表的头指针。
哈希算法
PHP哈希表最小容量是8(2^3),最大容量是0x80000000(2^31),并向2的整数次幂圆整(即长度会自动扩展为2的整数次幂,如13个元素的哈希表长度为16;100个元素的哈希表长度为128)。nTableMask被初始化为哈希表长度(圆整后)减1。具体代码在zend/Zend_hash.c的_zend_hash_init函数中,这里截取与本文相关的部分并加上少量注释。
01 | 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) |
02 | { |
03 | uint i = 3; |
04 | Bucket **tmp; |
05 |
06 | SET_INCONSISTENT(HT_OK); |
07 |
08 | //长度向2的整数次幂圆整 |
09 | if (nSize >= 0x80000000) { |
10 | /* prevent overflow */ |
11 | ht->nTableSize = 0x80000000; |
12 | } else { |
13 | while ((1U << i) < nSize) { |
14 | i++; |
15 | } |
16 | ht->nTableSize = 1 << i; |
17 | } |
18 |
19 | ht->nTableMask = ht->nTableSize - 1; |
20 |
21 | /*此处省略若干代码…*/ |
22 |
23 | return SUCCESS; |
24 | } |
值得一提的是PHP向2的整数次幂取圆整方法非常巧妙,可以背下来在需要的时候使用。
Zend HashTable的哈希算法比较简单:
1 | hash(key)=key & nTableMask |
即简单将数据的原始key与HashTable的nTableMask进行按位与即可。
如果原始key为字符串,则首先使用Times33算法将字符串转为整形再与nTableMask按位与。
1 | hash(strkey)=time33(strkey) & nTableMask |
下面是Zend源码中查找哈希表的代码:
01 | ZEND_API int zend_hash_index_find( const HashTable *ht, ulong h, void **pData) |
02 | { |
03 | uint nIndex; |
04 | Bucket *p; |
05 |
06 | IS_CONSISTENT(ht); |
07 |
08 | nIndex = h & ht->nTableMask; |
09 |
10 | p = ht->arBuckets[nIndex]; |
11 | while (p != NULL) { |
12 | if ((p->h == h) && (p->nKeyLength == 0)) { |
13 | *pData = p->pData; |
14 | return SUCCESS; |
15 | } |
16 | p = p->pNext; |
17 | } |
18 | return FAILURE; |
19 | } |
20 |
21 | ZEND_API int zend_hash_find( const HashTable *ht, const char *arKey, uint nKeyLength, void **pData) |
22 | { |
23 | ulong h; |
24 | uint nIndex; |
25 | Bucket *p; |
26 |
27 | IS_CONSISTENT(ht); |
28 |
29 | h = zend_inline_hash_func(arKey, nKeyLength); |
30 | nIndex = h & ht->nTableMask; |
31 |
32 | p = ht->arBuckets[nIndex]; |
33 | while (p != NULL) { |
34 | if ((p->h == h) && (p->nKeyLength == nKeyLength)) { |
35 | if (! memcmp (p->arKey, arKey, nKeyLength)) { |
36 | *pData = p->pData; |
37 | return SUCCESS; |
38 | } |
39 | } |
40 | p = p->pNext; |
41 | } |
42 | return FAILURE; |
43 | } |
其中zend_hash_index_find用于查找整数key的情况,zend_hash_find用于查找字符串key。逻辑基本一致,只是字符串key会通过zend_inline_hash_func转为整数key,zend_inline_hash_func封装了times33算法,具体代码就不贴出了。