本文是跟着《php7底层设计与源码实现》一书进行的梳理和个人理解。
在分析之前先来了解一下HashTable这个数据结构:
php的数组其实是非常强大的,既可以表示普通数组,又可以表示字典,然后再结合zval,什么数据类型都能存储。
了解完了“哈希表”的概念之后,我们先来画一个简单的HashTable示意图。
- key:键名,通过该键名可以快速检索到对应的value
- value:键值
- bucket:Bucket原意为桶,是用户用来管理所存储对象的存储空间,即存储数据的容器单元,即存储key和value等
- slot:槽,一个槽可以有多个bucket,而一个bucket必须从属于某个slot,可以理解为槽就是一个一个的宿舍楼,而bucket就是宿舍房间,key和value这些就是宿舍里面的人
- hash函数:通过hash函数可以将key映射到某个slot槽,可以理解为通过你的专业,可以查到你在哪一栋宿舍楼,进而就可以知道你在哪个宿舍了
- 哈希冲突:slot0后面的bucket后面连着另一个bucket,这是因为key通过hash函数计算后映射到的slot槽位置一致,即哈希冲突,而为了解决存储问题,上图中使用的方法就是链地址法
而实际上,php5在实现自己的HashTable结构时是有自己的考量的,下图就是实际php5中的HashTable结构示意图:
第一个变化,新增的h值:
- php数组的键名不仅可以是字符串还可以是数字,所以为了兼容这两种情况,“h”就代表数字key,“key”就代表字符串key
- 有了h值,在比较两个key字符串是否相等时,会首先比较两个key字符串的h值,然后再去比较字符串的长度以及内容,如果h值就不相等,就可以认为两个字符串不相等,这样可以加快两个字符串比较的速度
先来看php5的源码来分析下字段含义,然后再讨论下php5的HashTable的实现原理。
typedef struct bucket {
ulong h; /* Used for numeric indexing */
uint nKeyLength;
void *pData;
void *pDataPtr;
struct bucket *pListNext;
struct bucket *pListLast;
struct bucket *pNext;
struct bucket *pLast;
const char *arKey;
} Bucket;
typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements;
ulong nNextFreeElement;
Bucket *pInternalPointer; /* Used for element traversal */
Bucket *pListHead;
Bucket *pListTail;
Bucket **arBuckets;
dtor_func_t pDestructor;
zend_bool persistent;
unsigned char nApplyCount;
zend_bool bApplyProtection;
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;
首先看bucket结构体
- h:表示数字key或字符串key的h值。
- arKey:即字符串key
- pData和pDataPtr:即value值
- nKeyLength:arKey的长度。当nKeyLength=0时,表示是数字key
- pListLast、pListNext、pLast、pNext:4个指向bucket的指针 pListLast和pListNext指向的是全局链表的前一个和后一个bucket,为了实现数组的有序性,而pLast和pNext则是指向局部链表的前一个和后一个bucket,为了解决哈希冲突
HashTable结构体
- arBuckets:是一个指针,指向一段连续的内存空间,这段内存存储的是指向bucket的指针,每一个内存空间代表着一个slot,并且该slot存储的指针是局部链表的第一个bucket
- nTableSize:arBuckets指向的连续数组内存的长度,即slot的数量。最大值为2的31次方(因为nTableSize是int类型,而slot实际上就是数组,索引下标从0开始)。
- nTableMask:掩码。总是等于nTableSize - 1,即2的n次方-1,由于nTableMask的每一位都会是1,所以key在经过hash1函数计算出h值,h值再经过hash2函数映射到对应的slot,这个hash函数的算法其实就是
slot = h & nTableMask(这里的掩码的作用实质上就是对过大超过nTableSize的值进行矫正),参考:按位与
- nNumOfElements:bucket元素的个数。在php5中删除一个bucket元素会将其从全局链表和局部链表中真正删除掉,并释放bucket本身以及value占用的内存
- pListHead和pListTail:这两个指针分别指向全局链表的头和尾,这样可以保证php数组的有序性
举个例子:将4个key-value对插入数组中,按插入顺序,key分别为"a"、"b"、"c"、"d",并且假设"a"被映射到了slot1,其余三个被映射到了slot0。
php5数组设计上存在的问题:
- 每一个bucket都需要一次内存分配。因为bucket之间是通过指针相连的,链表的数据结构,元素在内存中是不连续的,内存分配也较为随机。
- 为了保证数组的元素有序性,每个bucket都要维护4个指针,在64位操作系统中,每个bucket就要有32个指针字节。如果有1024个bucket,就会额外多出32KB的内存,并且bucket的内存分配也是随机的,导致了CPU的cache命中率并不高,这样在遍历HashTable时,并没有很高的性能。
- 还有一个书中有提到,对于大部分场景,key-value中的value都是zval,这样的话,每个bucket需要维护指向zval的指针pDataPtr以及指向pDataPtr的pDataPtr指针,空间效率不高。(这点我还时不是太明白)
以上就是对于php5中数组的分析,大部分都是书中的内容,自己从头捋一遍会更加清晰。