PHP数组底层实现原理

需要实现的语义功能

  • 语义一: PHP数组是一个字典,存储着键值(keyvalue)对。通过键可以快速地找到对应的值,键可以是整型,也可以是字符串。
  • 语义二:PHP数组是有序的。这个有序指的是插入顺序,即遍历数组的时候,遍历元素的顺序应该和插入顺序一致,而不像普通字典一样是随机的

PHP5的数组实现

HashTable

Hashtable示意图
HashTable的结构定义:

typedef struct _HashTable {
	uint nTableSize;
	uint nTableMask;
	uint nNumOfElements;
	ulong nNextFreeElement;
	Bucket *pInternalPointer;
	Bucket *pListHead;
	Bucket *pListTail;
	Bucket **arBuckets;
	dtor_func_tp Destructor;
	zend_bool persistent;
	unsigned charnApplyCount;
	zend_bool bApplyProtection;
	#if ZEND_DEBUG
	int inconsistent;
	#endif
} HashTable;

HashTable中的成员变量:

  • arBuckets:是一个指针,指向一段连续的数组内存,这段数组内存并没有存储bucket,而是存储着指向bucket的指针。每一个指针代表着一个slot,并且指向slot局部链表的首元素。通过这个指针,可以遍历这个slot下的所有的bucket。
  • nTableSize:arBuckets指向的连续内存中指针的个数,即表示slot的数量。该字段取值始终是2的n次方,最小值是8,最大值为0x80000000(2的31次方)。当bucket数量大于slot数量时,肯定会存在某一个slot至少有两个bucket,随着slot下bucket数量的增多,HashTable逐渐退化成链表,性能会有严重下降。这时PHP5会进行扩容,将slot数量加倍,然后进行rehash,让bucket均匀分布在slot中
  • nTableMask: 掩码。总是等于nTableSize1,即2n1,因此,nTableMask的每一位都是1。上文提到的哈希过程中,key经过hash1函数,转为h值,h值通过hash2函数转为slot值。这里的hash2函数就是slot=h&nTableMask,进而通过arBuckets[slot]取得当前slot链表的头指针
  • nNumOfElements:bucket元素的个数。在PHP5中,删除某一个元素会将bucket从全局链表和局部链表中真正删除掉,并释放bucket本身以及value占用的内存
  • pListHead & pListTail: 为了保证数组的第二个语义(有序),HashTable维护了一个全局链表,这两个指针分别指向这个全局链表的头和尾。所以在PHP5中的遍历实现,其实是遍历了这个双向链表
Bucket
typedef struct bucket{
	ulongh;/*Usedfornumericindexing*/
	uintnKeyLength;
	void *pData;
	void*pDataPtr;
	structbucket *pListNext;
	structbucket *pListLast;
	structbucket *pNext;
	structbucket *pLast;
	constchar *arKey;
} Bucket;

Bucket结构示意图
Bucket中的成员变量:

  • arKey:对应HashTable设计中的key,表示字符串key。
  • h:对应HashTable设计中的h,表示数字key或者字符串key的h值
  • pData和pDataPtr:对应HashTable设计中的value。
    pData和pDataPtr都是指针,当value大小等于一个指针大小时就直接存储在pDataPtr中避免内存申请,减少内存碎片;其他情况value都存储在pData指向的内存空间,pDataPtr为null.
  • nKeyLength:arKey的长度。当nKeyLength等于0时,表示数字key。
    比较字符串key是否相等时,会先比较h值,如果h值相等,则不会直接比较字符串的内容,而是先比较字符串的长度是否相等。这样可以提高比较的速度。
  • pListLast、pListNext、pLast、pNext:4个指向bucket的指针。
    四个指针的作用,可以把他们看做两组,pListLast、pListNext 为全局链表指针,作用是满足语义1的要求,按插入顺序将所有的bucket串联起来,整个HashTable只有一个全局链表;pLast、pNext为局部链表,为了解决哈希冲突,每个slot维护着一个链表,使用链地址法解决冲突问题。

示例:PHP5顺序插入"a",“b”,“c”,"d"

在这里插入图片描述

PHP5数组设计上的问题
  • 每一个bucket都需要一次内存分配。尽管由于内存池的存在,不需要通过malloc函数直接申请系统内存,避免了系统调用在用户态和内核态之间的切换以及malloc函数额外开销所造成的空间浪费,但是内存申请的耗时还是存在并且不可忽略
  • 对于大部分场景,keyvalue中的value都是zval。这种情况下,每个bucket需要维护指向zval的指针pDataPtr以及指向pDataPtr的pData指针。空间效率不是很高。
  • 为了保证数组的两个语义,每一个bucket需要维护4个指向bucket的指针。在32位/64位系统,每个bucket将为这4个指针付出16字节/32字节。想象一下,对于拥有1024个bucket的HashTable,为了实现数组的两个语义,需要额外16KB/32KB的内存。而且由于bucket内存分配是随机的,导致了CPU的cache命中率并不高,这样在遍历HashTable的时候并没有很高的性能。

PHP7 中数组的实现

typedef struct _zend_array zend_array;
typedef struct _zend_array HashTable;
typedef struct _Bucket {
	zval val;
	zend_ulong h;/*hashvalue(ornumericindex)*/
	zend_string *key;
	/*stringkeyorNULLfornumerics*/
} Bucket;
struct _zend_array {
	zend_refcounted_h gc;
	union {
		struct {
		_ENDIAN_LOHI_4(
		zend_uchar flags,
		zend_uchar nApplyCount,
		zend_uchar nIteratorsCount,
		zend_uchar consistency)
		}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;
};

Bucket结构分析
在这里插入图片描述

  • val:对应HashTable设计中的value,始终是zval类型。
  • h:对应HashTable设计中的h,表示数字key或者字符串key的h值。
  • key:对应HashTable设计中的key,表示字符串key。区别于PHP5,这里不再是char*类型的指针,而是一个指向zend_string的指针。

Bucket使用状态划分

  • 未使用bucket:最初所有的bucket都是未使用的状态
  • 有效bucket: 存储着有效的数据(key、val、h),当进行插入时,会选择一个未使用bucket,这样该bucket就变成了有效bucket。
  • 无效bucket:当bucket上存储的数据被删除时,有效bucket就会变为无效bucket。同时,对于某些场景的插入(packedarray的插入,5.3.3节会提到),除了会生成一个有效bucket外,还会有副作用,生成多个无效bucket。

HashTable图例
在这里插入图片描述

  • gc:引用计数相关,在PHP7中,引用计数不再是zval的字段,而是被设计在zval的value字段所指向的结构体中。
  • arData:实际的存储容器。通过指针指向一段连续的内存,存储着bucket数组
  • nTableSize:HashTable的大小。表示arData指向的bucket数组的大小,即所有bucket的数量。该字段取值始终是 2 n 2^{n} 2n,最小值是8,最大值在32位系统中是0x40000000( 2 30 2^{30} 230),在64位系统中是0x80000000( 2 31 2^{31} 231
  • nNumUsed:指所有已使用bucket的数量,包括有效bucket和无效bucket的数量。
  • nNumOfElements:有效bucket的数量。该值总是小于或等于nNumUsed。
  • nTableMask:掩码。一般为nTableSize。
  • nInternalPointer:HashTable的全局默认游标
  • nNextFreeElement:HashTable的自然key。自然key是指HashTable的应用语义是纯数组时,插入元素无须指定key,key会以nNextFreeElement的值为准。该字段初始值是0
  • pDestructor:析构函数
  • u:是一个联合体。占用4个字节。可以存储一个uint32_t类型的flags,也可以存储由4个unsignedchar组成的结构体v,这里的宏ZEND_ENDIAN_LOHI_4是为了兼容不同操作系统的大小端
  • u.v.flags:用各个bit来表达HashTable的各种标记。
  • u.v.nApplyCount:递归遍历计数。为了解决循环引用导致的死循环问题,当对某数组进行某种递归操作时(比如递归count),在递归调用入栈之前将nApplyCount加1,递归调用出栈之后将nApplyCount减1。
  • u.v.nIteratorsCount:迭代器计数。
  • u.v.consistency:成员用于调试目的,只在PHP编译成调试版本时有效,表示HashTable的状态

HashTable计数示例

逻辑链表设计
在这里插入图片描述
为了解决哈希冲突问题所需要的slot被设计放到了索引数组中。索引数组中每个元素代表一个slot,存放slot链表的第一个元素再bucket数组中的下标,默认值为-1代表没有元素。而为了逻辑链表的实现,PHP7通过bucket.val.u2.next表达链表中下一个元素在数组中的下标。

参考:
PHP 7底层设计与源码实现

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值