老版本hashtable的实现
php中hashtable的大体结构上和别的语言没什么区别,有两部分构成hashtable和bucket 和链式存储。
来看下hashtable的定义
typedef struct _hashtable {
uint nTableSize; // 大小
uint nTableMask; // hashtable的掩码 大小nTableSize-1 用来计算hash值
uint nNumOfElements; // 实际存储元素的大熊啊
ulong nNextFreeElement; // 下一个空闲元素的位置 例如 a[] = 'test' 一定情况下快速定位
Bucket *pInternalPointer; // 当前遍历的指针 foreach 比for快的原因
Bucket *pListHead; // 指向第一个元素 保障了顺序遍历
Bucket *pListTail; // 指向最后一个元素
Bucket **arBuckets; // Bucket 数组 key:hash值 value是指向对应第一个bucket的指针
dtor_func_t pDestructor; // 析构函数
zend_bool persistent; //
unsigned char nApplyCount;
zend_bool bApplyProtection; // 和nApplyCount一起使用 防止无限递归 解决循环引用的问题
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;
bucket定义
typedef struct bucket {
/* Used for numeric indexing */
ulong h; // 对char *key进行hash后的值,数字索引的话就是索引值
uint nKeyLength; // hash关键字的长度,如果数组索引为数字,此值为0
void *pData; // 指向value,一般是用户数据的副本,如果是指针数据,则指向pDataPtr
void *pDataPtr; //如果是指针数据,此值会指向真正的value,同时上面pData会指向此值
struct bucket *pListNext; // 整个hash表的下一元素
struct bucket *pListLast; // 整个哈希表该元素的上一个元素
struct bucket *pNext; // 存放在同一个hash Bucket内的下一个元素
struct bucket *pLast; // 同一个哈希bucket的上一个元素
char arKey[1];
/*存储字符索引,此项必须放在最未尾,因为此处只字义了1个字节,存储的实际上是指向char *key的值,
这就意味着可以省去再赋值一次的消耗,而且,有时此值并不需要,所以同时还节省了空间。
*/
} Bucket;
hashtable的结构图
新版本的hashtable
与老版本的hashtable相比改动还是挺大的
- 老版本的元素存储是分散的,Bucket **arBuckets 里面存储的是指针 指向bucket的地址,新版的的元素存储是连续的 Bucket *arData;
- 老版本bucket中有4个指针 新版版中的bucket中只有一个指针,并且只有在hash碰撞的时候才会用到
少了三个指针,看下新版本的hashtable 如何做好按照插入顺序遍历和解决hash冲突
看下hashtable的定义
typedef struct _zend_array HashTable;
struct _zend_array {
zend_refcounted_h gc; // gc 相关
union { // 联合体
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar nApplyCount,
zend_uchar nIteratorsCount,
zend_uchar consistency)
} v;
uint32_t flags;
} u;
uint32_t nTableMask; // hash表的掩码 用来确定hsh
Bucket *arData; // bucket数组
uint32_t *arHash; // hashtable 查找 大小为nTableMask 存放指向bucket的指针(疑问在源码定义中未看到)
uint32_t nNumUsed; // 元素个数 包含已删除的元素
uint32_t nNumOfElements; // 有效的元素个数
uint32_t nTableSize; // hash表的大小
uint32_t nInternalPointer;
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};
bucket的定义
typedef struct _Bucket {
zval val;
zend_ulong h; //存的hash 值 用来寻找对比key
zend_string *key; // 如果key是string 则存放key 如果是数字 则为空
} Bucket;
typedef struct _zval_struct zval;
struct _zval_struct {
zend_value value; // value 真正的结构
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type, /* active type */
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved) /* call info for EX(This) */
} v;
uint32_t type_info;
} u1;
union {
uint32_t next; // 重点关注这个 存放hash 冲突下一个元素的位置
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 */
uint32_t access_flags; /* class constant access flags */
uint32_t property_guard; /* single property guard */
uint32_t extra; /* not further specified */
} u2;
};
新hashtable的结构图
可以看出,新的hashtable bucket使用一段连续的内存,按插入顺序依次增长。连续内存比老版本的元素分散存储要快很多,但是这样会有个问题。 元素的删除,导致索引的重建。解决办法:软删除 将val标志为UNDEF 在循环取值的判断val,代码如下
for (i = 0; i < ht->nNumUsed; ++i) {
Bucket *b = &ht->arData[i];
if (Z_ISUNDEF(b->val)) continue;
// do stuff with bucket
}
但是这样造成的内存的浪费, php在数组的扩容或需要调整的时候 才真正删除这些元素, 以空间换时间,实际上我们在使用的过程中 unset元素的操作极少。
在看下如何解决hash碰撞的
解决hash 碰撞是有两个指针解决,hashtable定义中的 *arHash 和 zval定义中的里面的 next。key hash之后 hash&nTableMask -1 获得在arHash数组中的位置,判断当前是否为null, 如为null则未产生hash碰撞,更新值为当前bucket的地址,如不为null 说明当前hash已有值,产生碰撞,设置当前bucket中的zval.next值为当前hash的值,当前hash值设为当前bucket的地址。形成如上图bk5和bk2的关系。 一个单链表解决了hash碰撞
hashtable的遍历 按arData依次遍历,foreach和for的效率基本一样
hashtable的查找 key hash之后定位在arHash的位置,一次遍历对应的单链表,判断 Bucket.h 或者 bucket.key 与key是否相等
一个疑问 新版本的hashtable是内存连续的,如果元素很多,超过操作系统单次申请的最大长度,如何处理的。