PHP内核中的哈希表是十分重要的数据结构,PHP的大部分的语言特性都是基于哈希表实现的, 例如:变量的作用域、函数表、类的属性、方法等,Zend引擎内部的很多数据都是保存在哈希表中的(在前面的章节中也介绍了hashTable的应用)。
Zend HashTable的实现结合了双向链表和向量(数组)两种数据结构的优点,为PHP提供了非常高效的数据存储和查询机制。
一、HashTable的数据结构
PHP实现HashTable主要是通过两个数据结构Bucket(桶)和HashTable。
从PHP脚本端来看,HashTable相当于Array对象,而Bucket相当于Array对象里的某个元素。对于多维数组实际就是HashTable的某个Bucket里存储着另一个HashTable。
①HashTable结构:
typedef struct _hashtable { uint nTableSize; // hash Bucket的大小,最小为8,以2x增长。 uint nTableMask; // nTableSize-1 , 索引取值的优化 uint nNumOfElements; // hash Bucket中当前存在的元素个数,count()函数会直接返回此值 ulong nNextFreeElement; // 下一个数字索引的位置 Bucket *pInternalPointer; // 当前遍历的指针(foreach比for快的原因之一) Bucket *pListHead; // 存储数组头元素指针 Bucket *pListTail; // 存储数组尾元素指针 Bucket **arBuckets; // 存储hash数组 dtor_func_t pDestructor; // 在删除元素时执行的回调函数,用于资源的释放 zend_bool persistent; //指出了Bucket内存分配的方式。如果persisient为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数。 unsigned char nApplyCount; // 标记当前hash Bucket被递归访问的次数(防止多次递归) zend_bool bApplyProtection;// 标记当前hash桶允许不允许多次访问,不允许时,最多只能递归3次 #if ZEND_DEBUG int inconsistent; #endif } 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;
二、HashTable的初始化
php为HashTable的初始化提供了一个接口zend_hash_init,这个接口主要是把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; //... if (nSize >= 0x80000000) { /* prevent overflow */ ht->nTableSize = 0x80000000; } else { while ((1U << i) < nSize) { i++; } ht->nTableSize = 1 << i; } // ... ht->nTableMask = ht->nTableSize - 1; /* Uses ecalloc() so that Bucket* == NULL */ if (persistent) { tmp = (Bucket **) calloc(ht->nTableSize, sizeof(Bucket *)); if (!tmp) { return FAILURE; } ht->arBuckets = tmp; } else { tmp = (Bucket **) ecalloc_rel(ht->nTableSize, sizeof(Bucket *)); if (tmp) { ht->arBuckets = tmp; } } return SUCCESS; }
参数nSize是要申请的桶数组的大小,但这并不是php实际申请的大小,因为php内部会计算出一个不小于nSize并且使2的n次方的数作为实现申请的大小。
参数persistent判断是否使用php内存管理,如果为FALSE就使用操作系统的内存管理,否则就是使用php内存管理。一般把这个参数设置为TRUE,因为使用操作系统内存管理需要自己释放内存,就容易造成内存泄露,所以最好交给php去管理内存。
三、HashTable的操作接口
PHP哈希表的操作接口实现。提供了如下几类操作接口:
- 初始化操作,例如zend_hash_init()函数,用于初始化哈希表接口,分配空间等。
- 查找,插入,删除和更新操作接口,这是比较常规的操作。
- 迭代和循环,这类的接口用于循环对哈希表进行操作。
- 复制,排序,倒置和销毁等操作。
在PHP中不管是对数组的添加操作(zend_hash_add),还是对数组的更新操作(zend_hash_update), 其最终都是调用_zend_hash_add_or_update函数完成,这在面向对象编程中相当于两个公有方法和一个公共的私有方法的结构, 以实现一定程度上的代码复用。
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) { //...省略变量初始化和nKeyLength <=0 的异常处理 h = zend_inline_hash_func(arKey, nKeyLength); nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; while (p != NULL) { if ((p->h == h) && (p->nKeyLength == nKeyLength)) { if (!memcmp(p->arKey, arKey, nKeyLength)) { // 更新操作 if (flag & HASH_ADD) { return FAILURE; } HANDLE_BLOCK_INTERRUPTIONS(); //..省略debug输出 if (ht->pDestructor) { ht->pDestructor(p->pData); } UPDATE_DATA(ht, p, pData, nDataSize); if (pDest) { *pDest = p->pData; } HANDLE_UNBLOCK_INTERRUPTIONS(); return SUCCESS; } } p = p->pNext; } p = (Bucket *) pemalloc(sizeof(Bucket) - 1 + nKeyLength, ht->persistent); if (!p) { return FAILURE; } memcpy(p->arKey, arKey, nKeyLength); p->nKeyLength = nKeyLength; INIT_DATA(ht, p, pData, nDataSize); p->h = h; CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]); //Bucket双向链表操作 if (pDest) { *pDest = p->pData; } HANDLE_BLOCK_INTERRUPTIONS(); CONNECT_TO_GLOBAL_DLLIST(p, ht); // 将新的Bucket元素添加到数组的链接表的最后面 ht->arBuckets[nIndex] = p; HANDLE_UNBLOCK_INTERRUPTIONS(); ht->nNumOfElements++; ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* 如果此时数组的容量满了,则对其进行扩容。*/ return SUCCESS; }
整个写入或更新的操作流程如下:
- 生成hash值,通过与nTableMask执行与操作,获取在arBuckets数组中的Bucket。
- 如果Bucket中已经存在元素,则遍历整个Bucket,查找是否存在相同的key值元素,如果有并且是update调用,则执行update数据操作。
- 创建新的Bucket元素,初始化数据,并将新元素添加到当前hash值对应的Bucket链表的最前面(CONNECT_TO_BUCKET_DLLIST)。
- 将新的Bucket元素添加到数组的链接表的最后面(CONNECT_TO_GLOBAL_DLLIST)。
- 将元素个数加1,如果此时数组的容量满了,则对其进行扩容。这里的判断是依据nNumOfElements和nTableSize的大小。 如果nNumOfElements > nTableSize则会调用zend_hash_do_resize以2X的方式扩容(nTableSize << 1)。
四、哈希表索引处理方式
在内核中只允许数字索引,对于字符串索引,内核采用了time33算法将字符串转换为整型。
1、数字索引处理
//省略了部分代码,提出主要的逻辑 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; }
2、字符串索引的处理
与数字索引相比,只是多了一步将字符串转换为整型。用到的算法是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; }
六、Zend引擎哈希表结构和关系
相关推荐
在PHP编程语言中,HashTable是一种核心数据结构,它是PHP实现数组、变量存储、函数、类等众多功能的基础。了解HashTable的实现对于深入理解PHP内部工作机制至关重要。本文将详细介绍PHP中HashTable的实现机制,并...
在PHP编程语言中,HashTable是核心的内部数据结构,用于存储和管理键值对。它在 Zend 引擎中扮演着至关重要的角色,被广泛应用于数组、对象、类属性等多种场景。深入理解HashTable的内部机制有助于提升PHP程序的性能...
02.PHP变量在内核中的实现.md 03.内存管理.md 04.配置编译环境.md 05.第一个扩展.md 06.函数的返回值.md 07.函数的参数.md 08.Array与HashTable.md 09.PHP中的资源类型.md 10.PHP中的面向对象上篇.md 11.PHP中的...
1. **面向对象编程**:理解类、对象、继承、封装和多态的概念,以及如何在PHP中实现它们。 2. **PHP扩展开发**:掌握ZEND_API,了解如何使用C语言编写PHP扩展,包括编写`.c`和`.h`文件,以及使用`phpize`和`make`...
在上一篇文章中,我们初步了解了PHP7变量内部实现的基础概念。在本文中,我们将深入探究PHP7变量实现的更多细节,以及它与PHP5之间的关键区别。理解这些知识对于深入掌握PHP7的工作原理非常重要。 PHP7中的变量通过...
7. Array与HashTable:介绍数组在PHP内核中的表现形式,如何操作HashTable以及在内核中操作PHP语言中的数组。 8. PHP中的资源类型:资源是PHP中用来表示外部资源的一种特殊类型,包括持久化资源和它们的引用计数...
在Zend HashTable的实现中,`Bucket`是存储数据的基本单元。它包含以下关键字段: - `ulong h`: 当`nKeyLength`为0时,`h`代表键的数值索引。 - `uint nKeyLength`: 键的长度,用于字符串键。 - `void *pData`: ...
在讨论PHP7内部变量实现之前,我们需要了解PHP5中变量的内部结构。PHP的变量存储在一个称为zval的数据结构中,其中包含了值(value)、类型(type)、引用计数(refcount)以及一个标志位(is_ref)来指示变量是否通过引用...
Hashtable在PHP中扮演关键角色,如变量符号栈、函数符号栈等都是基于hashtable实现。这在动态查找变量和函数时提供高效支持,但同时也带来了运行时的开销。 ### 7. 性能比较 PHP的性能并不像普遍认为的那么差。...
在PHP7中,内存分配主要通过两种方式实现:传统的`malloc`和`free`函数以及PHP自定义的`_emalloc`和`_efree`函数。 - **传统的`malloc`和`free`**: - `void* ptr = malloc(sizeof(...));` - `free(ptr);` 这种...
Tiny DB使用HashTable算法在PHP中实现。 示例:<?php include('CuteDB.php'); $ db =新的CuteDB(); $ db-> open('test'); //打开数据库$ db-> set('test_key','test_value'); CuteDB单个PHP文件。 Tiny...
PHP的变量在内存中的表示和管理是通过`zend_variable`结构体完成的,而符号表(`HashTable`)则存储了所有变量的信息。在PHP 5.5.3中,变量的生命周期管理和垃圾回收机制得到了进一步优化。 6. **对象模型** PHP ...
变量符号表是 PHP 的一个核心组件,用于实现变量的查找和赋值。 内存和文件 在 PHP 中,内存和文件是两个基本概念。内存是指 PHP 运行时的内存区域,用于存储变量和数据。文件是指 PHP 的文件系统,用于存储和读取...
在本文档中,我们将探讨如何使用PHP实现哈希表(Hash表)功能。哈希表是一种数据结构,它通过将键(Key)映射到数组中的特定位置来存储和检索数据,从而提供快速访问。哈希函数是这个过程的关键,它将任意长度的键...
理解如何在C代码中操作`HashTable`对于高效地编写PHP扩展至关重要。 ### 结论 通过深入了解`zval`结构、PHP数据处理方式、创建扩展的基本流程以及如何与数组和哈希表交互,你可以更好地掌握PHP扩展的开发。这不仅...
PHP中的数组实际上就是基于HashTable实现的。 #### 七、PHP变量 在PHP中,变量的类型不是固定的,而是动态确定的。以下是一些常见的变量类型及其特点: - **整数、浮点数类型变量**:表示数值数据,支持基本的...
数组和HashTable在PHP中的操作是内核应用的重要组成部分,涉及数组在C语言中的实现以及HashTable的API使用。资源类型是PHP中一个特殊的复合数据类型,介绍了资源的概念以及Persistent Resources的引用计数机制。面向...