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结构:

    
    
  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

猜你喜欢

转载自blog.csdn.net/ahaotata/article/details/84450741
今日推荐