从PHP 的 zval 结构体可以看出 PHP 使用HashTable来保存数组信息,PHP的HashTable使用了一些技巧,这些技巧是PHP高效数组操作的直接原因,源代码在PHP源代码目录 的Zend/zend_hash.h Zend/zend_hash.c 中。先来看看Zend HashTable的定义:
参数解释:
nTableSize 哈希表的大小
nTableMask 数值上等于 nTableSize -1
nNumOfElements 记录了当前 HashTable 中保存的记录数
nNextFreeElement 指向下一个空闲的 Bucket (之后有解释)
pInternalPointer
pListHead 指向 Bucket 列表头部
pListTail 指向 Bucket 列表尾部
arBuckets
pDestructor 一个函数指针,在 HashTable 发生增、删、改时自动调用,以完成某些清理工作。
persistent 是否是持久
nApplyCount
aApplyProtection 这两个参数用于放置在遍历时发生无限递归
可以看到Bucket 是一个双向链表,参数解释:
h 当元素使用数字索引时使用
nKeyLength 当使用字符串索引时,该选项表示字符串索引的长度,而字符串则保存在 Bucket 结构体的最后一个元素 arKey 中。尽管 arKey 被声明为一个只有一个元素的数组,但是这并不妨碍我们在其中保存字符串,因为数组名可以看做指针,将 arKey 作为结构体的最后一个元素则 Bucket 结构体就成了变长结构体,而该变长结构体的长度则需要 nKeyLength 的辅助才能确定,这是 C 语言中的常见技巧。
pNext指向具有相同 hash 值的下一个 bucket 元素,无论 HashTable 设计的如何完美,冲突都是难免的。当采用字符串索引时, h 成员变量存放的就是字符串索引的 hash 值。
pData指向保存的数据,如果数据本身又为指针,则用 pDataPtr 来保存对应的指针,而辞此时 pData 则指向自身结构体的 pDataPtr 。
接着看Zend HashTable 的一些相关函数 :
#define HASH_PROTECT_RECURSION(ht) /
if ((ht)->bApplyProtection) { /
if ((ht)->nApplyCount++ >= 3) { /
zend_error(E_ERROR, "Nesting level too deep - recursive dependency?" ); /
} /
}
这个宏用于防止循环引用。
#define ZEND_HASH_IF_FULL_DO_RESIZE(ht) /
if ((ht)->nNumOfElements > (ht)->nTableSize) { /
zend_hash_do_resize(ht); /
}
该宏用于判断HashTable 中的元素是否超过了 HashTable 表的大小,如果超过则扩展 HashTable 的大小,查看 zend_hash_do_resize 的代码可以看到每次扩展大小都是成倍的。
看看Zend 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; //这里可以看出数组默认的初始化为 8
Bucket **tmp;
SET_INCONSISTENT(HT_OK); // 用于调试
if (nSize >= 0×80000000) {
/* prevent overflow */
ht->nTableSize = 0×80000000;
} else {
while ((1U << i) < nSize) {
i++;
}
ht->nTableSize = 1 << i;
}
ht->nTableMask = ht->nTableSize - 1;
ht->pDestructor = pDestructor;
ht->arBuckets = NULL;
ht->pListHead = NULL;
ht->pListTail = NULL;
ht->nNumOfElements = 0;
ht->nNextFreeElement = 0;
ht->pInternalPointer = NULL;
ht->persistent = persistent;
ht->nApplyCount = 0;
ht->bApplyProtection = 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;
}
可以看到HashTable 的大小被自动的初始化为 2 的 n 次方, persistent 参数用于指示是否是“永久”方式分配内存,如果是则采用系统分配内存方法,否则采用ZendMM 的内存分配方式,关于 ZendMM 请搜索 PHP内存管理 的相关内容。
申请得到的bucket 指针内存块都放在 HashTable 的 arBucket 中,可以把这段内存块看成一个数组,数组中的每个元素都指向一个实际的 bucket 。
ZEND_API int _zend_hash_add_or_update(HashTable *ht, char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC)
{
ulong h;
uint nIndex;
Bucket *p;
IS_CONSISTENT(ht);
if (nKeyLength <= 0) {
#if ZEND_DEBUG
ZEND_PUTS( "zend_hash_update: Can’t put in empty key/n" );
#endif
return FAILURE;
}
//根据索引值和索引长度生成hash值
h = zend_inline_hash_func(arKey, nKeyLength);
//用hash值和nTableMask进行按位于运算,用于索引的快速定位
//按位于后的结果不可能大于nTableMask的值
//结合下面的代码,可以看出这段代码的巧妙
nIndex = h & ht->nTableMask;
p = ht->arBuckets[nIndex];
//如果p不为NULL,则产生了hash冲突
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();
#if ZEND_DEBUG
if (p->pData == pData) {
ZEND_PUTS( "Fatal error in zend_hash_update: p->pData == pData/n" );
HANDLE_UNBLOCK_INTERRUPTIONS();
return FAILURE;
}
#endif
//到了这里就说明是更新操作
//先调用原来的析构函数执行清理
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]);
if (pDest) {
*pDest = p->pData;
}
HANDLE_BLOCK_INTERRUPTIONS();
CONNECT_TO_GLOBAL_DLLIST(p, ht);
ht->arBuckets[nIndex] = p;
HANDLE_UNBLOCK_INTERRUPTIONS();
ht->nNumOfElements++;
ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* If the Hash table is full, resize it */
return SUCCESS;
}
看到这里就可以发现多数代码都是类似的了,
#define CONNECT_TO_BUCKET_DLLIST(element, list_head) /
(element)->pNext = (list_head); /
(element)->pLast = NULL; /
if ((element)->pNext) { /
(element)->pNext->pLast = (element); /
}
这个宏用于将一个bucket 加入到 bucket 链表中
#define CONNECT_TO_GLOBAL_DLLIST(element, ht) /
(element)->pListLast = (ht)->pListTail; /
(ht)->pListTail = (element); /
(element)->pListNext = NULL; /
if ((element)->pListLast != NULL) { /
(element)->pListLast->pListNext = (element); /
} /
if (!(ht)->pListHead) { /
(ht)->pListHead = (element); /
} /
if ((ht)->pInternalPointer == NULL) { /
(ht)->pInternalPointer = (element); /
}
该宏用于将一个bucket 加入到 HashTable 的链表中
下面列出zend 封装好的函数或者宏:
zend_hash_add_empty_element 给数组增加一个空元素
zend_hash_do_resize 扩大哈希表的大小
_zend_hash_index_update_or_next_insert 插入或者更新指定数字索引的元素
zend_hash_del_key_or_index 根据索引删除HashTable 中的元素
zend_hash_apply 遍历HashTable ,注意当中使用了两个宏 HASH_PROTECT_RECURSION 和 HASH_UNPROTECT_RECURSION 来防止遍历陷入死循环。
#define HASH_PROTECT_RECURSION(ht) /
if ((ht)->bApplyProtection) { /
if ((ht)->nApplyCount++ >= 3) { /
zend_error(E_ERROR, "Nesting level too deep - recursive dependency?" ); /
} /
}
#define HASH_UNPROTECT_RECURSION(ht) /
if ((ht)->bApplyProtection) { /
(ht)->nApplyCount–; /
}
zend_hash_reverse_apply 反向遍历HashTable
zend_hash_copy 拷贝
_zend_hash_merge 合并
zend_hash_find 字符串索引方式查找
zend_hash_index_find 数值索引方法查找
zend_hash_quick_find 上面两个函数的封装
zend_hash_exists 是否存在索引
zend_hash_index_exists 是否存在索引
zend_hash_quick_exists 上面两个方法的封装
ZEND_API int zend_hash_num_elements(HashTable *ht)
{
IS_CONSISTENT(ht);
return ht->nNumOfElements;
}
获得数组大小
为了更加方便的操作HashTable,Zend将上面的宏做了进一步的封装。