作为php的重要数据结构,本博记录一下hashtable的结构,存储,hash定位等问题。
- hashtable结构,是有56个字节组成等struct,如下:
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;
Bucket *arData;
uint32_t nNumUsed;
uint32_t nNumOfElements;
uint32_t nTableSize;
uint32_t nInternalPointer;
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};
最重要的就是arData,是指向Bucket类型的指针,Bucket的结构定义如下:
typeof struct _Bucket{
zval val;
zend_ulong h;
zend_string *key;
} Bucket;
Bucket不再使用指向zval类型的指针,而是直接使用数据本身,arData的结构如下:
可以看出arData中数据都是顺序存储的,这样有几个好处,因为在内存中是相邻的,迭代器逻辑变得简单,直接便利arData,内存相邻,可以极大的利用CPU缓存。
数据增加删除
新增加数据都是顺序存入arData中,每新增一个,ht->nNumUsed++,当达到hashtable最大值,触发压缩扩容算法。数据删除时设置值为UNDEF的zval。hash定位
因为arData是顺序存储,所以经过hash的值不能作为arData的索引,而是通过一张转化表,将hash值转化为arData的索引,如图:
转化表是以arData起始指针为起点做镜面映射,所以在分配arData时,反射表也同时分配了。hash冲突
php采用链地址法解决hash冲突,但和普通链表不一样的是,直接读取整个arData,因为内存相邻,极大的利用cpu缓存,这也是php7性能提升的重要原因。转化表和哈希表初始化
是分两次初始化,先只创建2个转化表节点,当有数据插入时才会完整初始化,这也是为什么极大节省了创建空数组开销的原因。
–hash表碎片重组扩容
因为顺序存储,删除的数据只标记为UNDEF,所以存在大量碎片,每次hash表扩容都是成倍增加,会浪费大量空间。