PHP数组详解

PHP数组的定义

  1. PHP的数组是一个字典。存储着(key-value)的键值对,可以通过key值快速查找到数组,key可以为整形,或者是字符串。
  2. PHP数组是有序的。这里面的顺序指的是插入顺序,即遍历元素的顺序应该和插入的顺序一致。

PHP数组的概念

PHP数组zend_array对应的是HashTable(也称作散列表),通过某种哈希函数将制定的键映射到对应的值的一种数组结构。因为它让key和value保持着一 一 对 应 关系,所以可以快速根据key查找到对应的值,查找效率O(1)

HashTable中元素介绍

在这里插入图片描述

  • bucket :桶,HashTable中存储数据的单元。用来存储key,value
  • slot :槽,HashTable拥有多个槽,bucket需要存储在slot中,一个slot可以拥有多个bucket
  • 哈希函数:用于计算bucket所属的slot
  • 哈希冲突:多个key进行计算后可能会出现属于同一个slot的情况(在文章最后会介绍PHP7的处理方法 )。

数组的分类

pakced array 和 hash array

基本认识

packed array :纯属组,不定义 key 值 ,;
hash array:基于key-value 的 map

packed array特性

  • key 全部为数字
  • key 按插入顺序排列,且为递增的
  • 每一个 key - value 的位置都是固定的,存储在bucket数组的第 key 个元素上
  • packed array 不需要索引数组 (索引数组大小为2)

packed array 的实现示意图
在这里插入图片描述

与packed array 不同,hash array 需要通过 key 计算出 h 值存储在 bucket 数组内,通过计算需要索引数组内的 slot 值,依次往下寻找到对应的值

在这里插入图片描述

按照上面的特性我们基本可以判断一个数组是否为 packed array ,但仍然存在特例情况:

$a = array (1=>‘a’ , 3=> ‘b’ , 5 => ‘c’); (例1)
$a = array (1=>‘a’ , 5=> ‘b’ , 3 => ‘c’); (例2)
$a = array (1=>‘a’ , 7 => ‘c’);(例3)

上面例1,例2凭借上面的 packed array特性 可以很明显的判断出,例1 为 packed array ,例2 为 hash array(两例子全部为数字,但插入顺序例1 是递增的);

例1 packed array 图示:
在这里插入图片描述

但例3 几乎与例1 相同,但实际上却是 hash array,因为在以 packed array 插入时,因只设置了 index 为 1 和 7的位置,中间的连续 bucket 未被使用,比较浪费空间;因此在空间效率和时间效率进行了一个平衡,在空间效率浪费比较多的时候,空间效率反而不如 hash array

以 packed array的方式插入 例3
在这里插入图片描述

以 hash array 的方式插入 例3
在这里插入图片描述

PHP5与PHP7的数组比较

PHP5数组

bucket结构
在这里插入图片描述

  • arKey:对应HashTable中的key
  • h:对应HashTable中的h,数字key或者字符串key的h值
  • pData和pDataPtr:对应HashTable中的value ;pData和pDataPtr都是指针;但如果value的值大小等于一个指针的大小,那么将直接存储到pDataPtr上,然后pData指向pDataPtr
  • nKeyLength:arKey的长度。当nKeyLength为0时,表示数字key。

整个HashTable维护一个全局链表,每个slot维护一个局部链表

  • pLast 和 pNext分别指向局部链表的前后 bucket
  • pListNext 和 pListLast 分别指向全局链表的前后 bucket

HashTable成员变量
在这里插入图片描述

  • arBuckets:指针;指向一段连续的数组内存,这段数组内存指向 bucket 的指针。每一个指针代表一个 slot,并且指向 slot 的数组首元素。通过这个指着可以遍历 slot 下的所有元素
  • nTableSize:arBuckets指向的连续内存中指针的个数,即 slot 的数量。该字段取值2的n次方,最小值为8,最大值为2的31次方;当bucket的数量大于slot时,则表示一个slot中存在多个 bucket ,随着bucket数量增多,会导致性能下降;这是PHP5会进行扩容,将 slot 数量翻倍,进行 rehash ,让 bucket 均匀分布
  • nTableMask:数值为nTableSize - 1 ;nTableMask的每一位都是1。上文提到的hash2函数就是slot=h&nTableMask,进而获得当前 slot 的头指针:arBuckets[slot]
  • nNumOfElements:bucket元素的个数。
  • pListHead和pListTail:这两个指针分别指向全局链表的头和尾,保证数组的有序性。

PHP5数组的缺点

  • 每个 bucket 都需要内存分配。
  • 每个 bucket 都需要维护 pData 和 pDataStr 指针,但大多数情况下用不到
  • 为了保证数组的一一对应,和顺序性,每个bucket都需要维护4个指向bucket的指针。对于拥有1024个bucket的HashTable,就需要额外的16KB/32KB的内存。且bucket内存是随机分配的,导致命中率不高

PHP7的数组

PHP7优化了数组的为了保证顺序性的链表设计,取消了全局链表的指针;PHP5是物理链表,PHP7是逻辑链表bucket 存储在一段连续的数组内存中,每一个bucket只维护到下一个 bucket 在数组中的索引;

因无需维护物理指针,PHP7中的 bucket 值存在 value ,key ,h 3个字段;key 这里不再是char* 型的指针,而是一个指向 zend_string 的指针。zend_string是一种带有字符串长度,h值,gc信息的字符数组的包装(后续插入中的图示中可以见到)。

Bucket的分类

从使用角度可以分为3种:

  • 未使用bucket:最初所有的bucket都是未使用状态
  • 有效bucket:存在着有效数据,当插入时未使用bucket变成有效bucket。更新操作只发生在有效bucket上,更新后仍为有效bucket
  • 无效bucket:当有效bucket存储的数据被删除时,有效bucket就会变为无效bucket。对于某些场景的插入可能会出现一个有效bucket和多个无效bucket

在内存的分布上,有效bucket和无效的bucket会交替分布,且都在未使用bucket前面。当删除的数据越来越多时,无效的bucket就会变多。这时会对整个bucket进行rehash操作,部分无效的bucket会变为有效的bucket 紧密分布,一部分会变为未使用bucket

bucket状态转换示意图
在这里插入图片描述

Rehash操作

rehash 的主要功能就是把 is_undef 的数据剔除,把有效的数据重新聚合到 bucket 数组并更新插入索引表;

重新建立索引:
在这里插入图片描述

rehash 不重新申请内存,在结构上做聚合调整

聚合操作:
在这里插入图片描述

PHP7的HashTable

在这里插入图片描述

  • arData:存储容器;实际指向一段连续的内存,存储bucket数组
  • nTableSize:HashTable的大小。表示arData指向的数组大小
  • nNumUsed:表示已使用bucket的数量,包括已使用bucket和无效bucket的数量。下标从0 ~ nNumUsed - 1 的 bucket都属于已使用bucket。而下标 nNumUsed 到 nTableSize - 1 都属于未使用的bucket
  • nNumElements:有效bucket的数量。该值小于或等于nNumUsed
  • nTableMask:掩码。通常是负数,一般为 - nTableSize
  • nInternalPointer:HashTable的全局默认游标
  • nNextFreeElement:HashTable中的自然key。自然key是指HashTable的应用情况时纯数组时,插入元素无需指定key,即 $arr[] = 1,会自动插入到key等于0的位置,此时nNextFreeElement 会变为 1

数组初始化

数组初始化步骤:

  1. 分配 HashTable 结构体内存,初始化各个字段 : 设置引用计数 gc_refcount , 类别为 数组,设置最初数组大小(2^n 最小为8),引用指针等
  2. 分配 bucket 数组内存,修改一些字段值

初始化后的 HashTable

在这里插入图片描述

  • nTableMask : -2 索引表大小,由上文我们可知 packed array 未使用到索引表,即值固定为 2
  • nInternalPoInter:-1 初始化尚未分配 arData,无遍历下标
插入,更新,查找,删除

插入,更新,查找,删除,这几个操作可以说是比较简单的,可以理解成从 bucket 中找到对应数据,然后进行需要的操作;下面通过几个实例语句与图示了解这几个操作

首先数组初始化:

  • nTableMask : -2
  • nTableSize: 8
  • nInternalPointer : -1

在这里插入图片描述

$arr[] = 'foo'

在这里插入图片描述
nNumUsed 修改为1,nNumOfElements 修改为1,同时将 foo 对应的 zend_string 拷贝到第一个 bucket 中的 zval 中

$arr['a'] = 'bar'

在这里插入图片描述
对于指定 key 值的赋值,直线根据 key = ‘a’ 进行查找,因查找不到对应 key ,选择进行插入到 HashTable 中

$arr[] = 'xyz'
$arr[2] = 'abc'
$arr['a'] = 'foo'

在这里插入图片描述

对于 $arr[‘a’] = ‘foo’ , 通过zend_string_hash_val 可以计算出 h 值,发现与第一个 bucket 中 key 为 ‘a’ 的 h 值相同,然后计算 nIndex ,得到对应位置 , 从 arData 中找到 第1个位置, 判断 key 值是否 为 ‘a’ ,然后修改对应的值为 ‘foo’

unset($arr['a'])

删除操作通过 key 值查找到 ‘a’ 对应的bucket,将 arData[-2] 对应的值修改为 -1 ,修改实际存储数量 nNumOfElements 减 1

在这里插入图片描述

哈希冲突

上面我们知道字符串key会经过hash运算得到对应的 h 值,但不同的key 值运算后的结果可能相同,那么在插入的时候就会造成冲突; PHP7 的处理方法是把每个冲突的 idx 放在 bucket 的 u2.next 中,插入时将老的 value 存储地址(idx)放到新 value 的next 中 ,然后将新 value 存储地址放入数组 ,如存在连续多个 nIndex 值相同 ,按上述规则向后添加 , 遍历是按照 u2.next 的值 逐步判断是否为对应的值
在这里插入图片描述


引用书籍
PHP7底层设计与源码实现

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值