PHP数组的底层实现
解决什么问题(概念):
PHP数组是一个字典,存储着键值(key=>value)的形式,可以通过键可以快速的找到对应的值,键可以是整形,也可以是字符串。
PHP数组是有序的。有序指的是插入顺序,遍历数组的时候,遍历元素的顺序应该与插入顺序一致。
通过什么方式实现的(原理):
PHP数组的实现是依赖于散列表(HashTable)实现的。散列表主要包含两个部分 存储元素数组以及散列函数。
PHP数组依赖的散列表数据结构定义如下(位于Zend/zend_types.h)
这个散列表中有很多成员,我们挑几个比较重要的来讲讲:
arData:散列表中保存存储元素的数组,其内存是连续的,arData指向数组的起始位置;
nTableSize:数组的总容量,即可以容纳的元素数,arData的内存大小就是根据这个值确定的,它的大小的是 2 的幂次方,最小为 8,然后按照 8、16、32...依次递增;
nTableMask:这个值在散列函数根据 key 的哈希值映射元素的时候用到,它的值实际就是nTableSize的负数,即nTableMask = -nTableSize,用位运算来表示就是nTableMask = ~nTableSize + 1;
nNumUsed、nNumOfElements:nNumUsed是指数组当前使用的Bucket数,但不是数组有效元素个数,因为某个数组元素被删除后并没有立即从数组中删除,而是将其标记为IS_UNDEF,只有在数组需要扩容时才会真正删除,nNumOfElements则表示数组中有效的元素数量,即调用count函数返回值,如果没有扩容,nNumUsed一直递增,无论是否删除元素;
nNextFreeElement:这个是给自动确定数值索引使用的,默认从 0 开始,比如$arr[] = 200,这个时候nNextFreeElement值会自动加 1;
pDestructor:当删除或覆盖数组中的某个元素时,如果提供了这个函数句柄,则在删除或覆盖时调用此函数,对旧元素进行清理;
u:这个联合体结构主要用于一些辅助作用。
Bucket 的结构比较简单,主要用来保存元素的 key 和 value,以及一个整型的 h(散列值,或者叫哈希值):如果元素是数值索引,则其值就是数值索引的值;如果是字符串索引,那么其值就是 key 通过 Time33 算法计算得到的散列值,h 的值用来最终映射元素的存储位置。Bucket 的数据结构如下:
PHP除了具备key=>value的形式外,还是有序的,HashTable并不满足有序,PHP数组做到顺序读取主要是依靠中间映射表。
PHP 数组底层结构中并没有显式标识这个中间映射表,而是与 arData 放到了一起,在数组初始化的时候并不仅仅分配用于存储 Bucket 的内存,还会分配相同数量的 uint32_t 大小的空间,这两块空间是一起分配的,然后将 arData 偏移到存储元素数组的位置(即上图中箭头线显示的位置,左侧为中间映射表,右边为bucket),而这个中间映射表就可以通过 arData 向前访问到。
中间映射表是一个与Bucket相同的数组,数组中储存整型数据,用于保存元素实际储存的Value在Bucket中的下标。Bucket中的数据是有序的,而中间映射表的数据是无序的。
映射函数
nIndex = h | ht->nTableMask;
将 key 经过 time33 算法生成的哈希值 h 和 nTableMask 进行或运算即可得出映射表的下标,其中 nTableMask 数值为 nTableSize 的负数。并且由于 nTableSize 的值为 2 的幂次方,所以 nTableMask 二进制位右侧全部为 0,保证了 h | ht->nTableMask 的取值范围会在 [-nTableSize, -1] 之间,正好在映射表的下标范围内。另外,用按位或运算的方法和其他方法如取余的方法相比运算速度较高,这个映射函数可以说设计的非常巧妙了。
流程图如下