参考资料
PHP7数组底层结构
//Bucket:散列表中存储的元素typedef struct _Bucket {zval val; //存储的具体value,这里嵌入了一个zval,而不是一个指针zend_ulong h; //key根据times 33计算得到的哈希值,或者是数值索引编号zend_string *key; //存储元素的key} Bucket;//HashTable结构typedef struct _zend_array HashTable;struct _zend_array {zend_refcounted_h gc;union {struct {ZEND_ENDIAN_LOHI_4(zend_uchar flags,zend_uchar nApplyCount,zend_uchar nIteratorsCount,zend_uchar reserve)} v;uint32_t flags;} u;uint32_t nTableMask; //哈希值计算掩码,等于nTableSize的负值(nTableMask = -nTableSize)Bucket *arData; //存储元素数组,指向第一个Bucketuint32_t nNumUsed; //已用Bucket数uint32_t nNumOfElements; //哈希表有效元素数uint32_t nTableSize; //哈希表总大小,为2的n次方uint32_t nInternalPointer;zend_long nNextFreeElement; //下一个可用的数值索引,如:arr[] = 1;arr["a"] = 2;arr[] = 3; 则nNextFreeElement = 2;dtor_func_t pDestructor;};
特性
1. 惰性删除
将哈希表中元素删除的时候不会直接将对应的Bucket移除,而是将此Bucket存储的zval设置为 IS_UNDEF ,使用 nNumUsed 去表示当前哈希表的总使用量,使用 nNumOfElements 表示有效元素数。只有在扩容的时候发现 ht->nNumUsed - ht->nNumOfElements > (ht->nNumOfElements >> 5) 的时候才会将已经被删除的元素全部移除。
2. 顺序插入
注意结构体中的 arData ,这个值指向存储元素数组的第一个Bucket,插入元素时按顺序 依次插入 数组,比如第一个元素在arData[0]、第二个在arData[1]…arData[nNumUsed]。PHP数组的有序性正是通过arData保证的,这是与普通散列表实现不同的地方。
3. 特殊的散列映射方式
在PHP7中,其实散列的映射关系也是存储在 arData 中,只不过是存在所谓的“负索引”中,在初始化一个数组的时候,分配内存时这个散列表与 arData 一起分配,而结构体中的 arData 指针并不是指向地址的起始位置, arData[0] ,arData[1] 用于访问哈希中的元素,arData[-1] ,arData[-2] ,用于访问哈希表中的哈希值与 arData 后半部分顺序数组的hash映射。
所以,整体来看HashTable主要依赖arData实现元素的存储、索引。插入一个元素时先将元素按先后顺序插入Bucket数组,位置是idx,再根据key的哈希值映射到散列表中的某个位置nIndex,将idx存入这个位置;查找时先在散列表中映射到nIndex,得到value在Bucket数组的位置idx,再从Bucket数组中取出元素。
图中Bucket的zval.u2.next默认值应该为-1,不是0
4. 哈希碰撞的解决
一般解决方法是将Bucket串成链表,查找时遍历链表比较key
PHP的实现也是如此,只是将链表的指针指向转化为了数值指向(即存储在 arData 中的数组下标),并且此数值是放在Bucket中存储的zval中:
struct _zval_struct {zend_value value; /* value */...union {uint32_t var_flags;uint32_t next; /* hash collision chain */uint32_t cache_slot; /* literal cache slot */uint32_t lineno; /* line number (for ast nodes) */uint32_t num_args; /* arguments number for EX(This) */uint32_t fe_pos; /* foreach position */uint32_t fe_iter_idx; /* foreach iterator index */} u2;};
当碰到哈希碰撞时,后插入的值将被放在冲突链的头部。
5. 扩容
PHP散列表的大小为2^n,插入时如果容量不够则首先检查已删除元素所占比例,如果达到阈值(ht->nNumUsed - ht->nNumOfElements > (ht->nNumOfElements >> 5),则将已删除元素移除,重建索引,如果未到阈值则进行扩容操作,扩大为当前大小的2倍,将当前Bucket数组复制到新的空间,然后重建索引。
//zend_hash.cstatic void ZEND_FASTCALL zend_hash_do_resize(HashTable *ht){if (ht->nNumUsed > ht->nNumOfElements + (ht->nNumOfElements >> 5)) {//只有到一定阈值才进行rehash操作zend_hash_rehash(ht); //重建索引数组} else if (ht->nTableSize < HT_MAX_SIZE) {//扩容void *new_data, *old_data = HT_GET_DATA_ADDR(ht);//扩大为2倍,加法要比乘法快,小的优化点无处不在...uint32_t nSize = ht->nTableSize + ht->nTableSize;Bucket *old_buckets = ht->arData;//新分配arData空间,大小为:(sizeof(Bucket) + sizeof(uint32_t)) * nSizenew_data = pemalloc(HT_SIZE_EX(nSize, -nSize), ...);ht->nTableSize = nSize;ht->nTableMask = -ht->nTableSize;//将arData指针偏移到Bucket数组起始位置HT_SET_DATA_ADDR(ht, new_data);//将旧的Bucket数组拷到新空间memcpy(ht->arData, old_buckets, sizeof(Bucket) * ht->nNumUsed);//释放旧空间pefree(old_data, ht->u.flags & HASH_FLAG_PERSISTENT);//重建索引数组:散列表zend_hash_rehash(ht);...}...}
6. 散列表的重建
当删除元素达到一定数量或扩容后都需要重建散列表,因为value在Bucket位置移动了或哈希数组nTableSize变化了导致key与value的映射关系改变,重建过程实际就是遍历Bucket数组中的value,然后重新计算映射值更新到散列表,除了更新散列表之外,这里还有一个重要的处理:移除已删除的value,开始的时候我们说过,删除value时只是将value的type设置为IS_UNDEF,并没有实际从Bucket数组中删除,如果这些value一直存在那么将浪费很多空间,所以这里会把它们移除,操作的方式也比较简单:将后面未删除的value依次前移,具体过程如下:
注意:在rehash的过程中,被删除的元素将被直接移除
//zend_hash.cZEND_API int ZEND_FASTCALL zend_hash_rehash(HashTable *ht){Bucket *p;uint32_t nIndex, i;...i = 0;p = ht->arData;if (ht->nNumUsed == ht->nNumOfElements) { //没有已删除的直接遍历Bucket数组重新插入索引数组即可do {nIndex = p->h | ht->nTableMask;Z_NEXT(p->val) = HT_HASH(ht, nIndex);HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(i);p++;} while (++i < ht->nNumUsed);} else {do {if (UNEXPECTED(Z_TYPE(p->val) == IS_UNDEF)) {//有已删除元素则将后面的value依次前移,压实Bucket数组......while (++i < ht->nNumUsed) {p++;if (EXPECTED(Z_TYPE_INFO(p->val) != IS_UNDEF)) {ZVAL_COPY_VALUE(&q->val, &p->val);q->h = p->h;nIndex = q->h | ht->nTableMask;q->key = p->key;Z_NEXT(q->val) = HT_HASH(ht, nIndex);HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(j);if (UNEXPECTED(ht->nInternalPointer == i)) {ht->nInternalPointer = j;}q++;j++;}}......ht->nNumUsed = j;break;}nIndex = p->h | ht->nTableMask;Z_NEXT(p->val) = HT_HASH(ht, nIndex);HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(i);p++;}while(++i < ht->nNumUsed);}}
