PHP7数组的底层实现示例


Posted in PHP onAugust 25, 2019

PHP 数组具有的特性

PHP 的数组是一种非常强大灵活的数据类型,在讲它的底层实现之前,先看一下 PHP 的数组都具有哪些特性。

可以使用数字或字符串作为数组健值

$arr = [1 => 'ok', 'one' => 'hello'];

可按顺序读取数组

foreach($arr as $key => $value){
 echo $arr[$key];
}

可随机读取数组中的元素

$arr = [1 => 'ok', 'one' => 'hello', 'a' => 'world'];

echo $arr['one'];

echo current($arr);

数组的长度是可变的

$arr = [1, 2, 3];

$arr[] = 4;

array_push($arr, 5);

正是基于这些特性,我们可以使用 PHP 中的数组轻易的实现集合、栈、列表、字典等多种数据结构。那么这些特性在底层是如何实现的呢? 这就得从数据结构说起了。

数据结构

PHP 中的数组实际上是一个有序映射。映射是一种把 values 关联到 keys 的类型。

PHP 数组的底层实现是散列表(也叫 hashTable ),散列表是根据键(Key)直接访问内存存储位置的数据结构,它的key - value 之间存在一个映射函数,可以根据 key 通过映射函数得到的散列值直接索引到对应的 value 值,无需通过关键字比较,在理想情况下,不考虑散列冲突,散列表的查找效率是非常高的,时间复杂度是 O(1)。

从源码中我们可以看到 zend_array 的结构如下:

typedef struct _zend_array zend_array;
typedef struct _zend_array hashTable;

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; // 哈希值计算掩码,等于nTableSize的负值(nTableMask = -nTableSize)
  Bucket      *arData;   // 存储元素数组,指向第一个Bucket
  uint32_t     nNumUsed;  // 已用Bucket数(含失效的 Bucket)
  uint32_t     nNumOfElements; // 哈希表有效元素数
  uint32_t     nTableSize;   // 哈希表总大小,为2的n次方(包括无效的元素)
  uint32_t     nInternalPointer; // 内部指针,用于遍历
  zend_long     nNextFreeElement; // 下一个可用的数值索引,如:arr[] = 1;arr["a"] = 2;arr[] = 3; 则nNextFreeElement = 2;
  dtor_func_t    pDestructor;
};

该结构中的 Bucket 即储存元素的数组,arData 指向数组的起始位置,使用映射函数对 key 值进行映射后可以得到偏移值,通过内存起始位置 + 偏移值即可在散列表中进行寻址操作。

Bucket 的数据结构如下:

typedef struct _Bucket {
  zval       val; // 存储的具体 value,这里是一个 zval,而不是一个指针
  zend_ulong    h;  // 数字 key 或字符串 key 的哈希值。用于查找时 key 的比较  
  zend_string   *key; // 当 key 值为字符串时,指向该字符串对应的 zend_string(使用数字索引时该值为 NULL),用于查找时 key 的比较
} Bucket;

到这里有个问题出现了:存储在散列表里的元素是无序的,PHP 数组如何做到按顺序读取的呢?

答案是中间映射表,为了实现散列表的有序性,PHP 为其增加了一张中间映射表,该表是一个大小与 Bucket 相同的数组,数组中储存整形数据,用于保存元素实际储存的 Value 在 Bucekt 中的下标。Bucekt 中的数据是有序的,而中间映射表中的数据是无序的。

PHP7数组的底层实现示例

而通过映射函数映射后的散列值要在中间映射表的区间内,这就对映射函数提出了要求。

映射函数

PHP7 数组采用的映射方式:

nIndex = h | ht->nTableMask;

将 key 经过 time33 算法生成的哈希值 h 和 nTableMask 进行或运算即可得出映射表的下标,其中 nTableMask 数值为 nTableSize 的负数。并且由于 nTableSize 的值为 2 的幂次方,所以 nTableMask 二进制位右侧全部为 0,保证了 h | ht->nTableMask 的取值范围会在 [-nTableSize, -1] 之间,正好在映射表的下标范围内。另外,用按位或运算的方法和其他方法如取余的方法相比运算速度较高,这个映射函数可以说设计的非常巧妙了。

散列(哈希)冲突

不同键名的通过映射函数计算得到的散列值有可能相同,此时便发生了散列冲突。

对于散列冲突有以下 4 种常用方法:

1.将散列值放到相邻的最近地址里

2.换个散列函数重新计算散列值

3.将冲突的散列值统一放到另一个地方

4.在冲突位置构造一个单向链表,将散列值相同的元素放到相同槽位对应的链表中。这个方法叫链地址法,PHP 数组就是采用这个方法解决散列冲突的问题。

其具体实现是:将冲突的 Bucket 串成链表,这样中间映射表映射出的就不是某一个元素,而是一个 Bucket 链表,通过散列函数定位到对应的 Bucket 链表时,需要遍历链表,逐个对比 Key 值,继而找到目标元素。而每个 Bucket 之间的链接则是将原 value 的下标保存到新 value 的 zval.u2.next 里,新 value 放在当前位置上,从而形成一个单向链表。

举个例子:

当我们访问 $arr['key'] 的过程中,假设首先通过散列运算得出映射表下标为 -2 ,然后访问映射表发现其内容指向 arData 数组下标为 1 的元素。此时我们将该元素的 key 和要访问的键名相比较,发现两者并不相等,则该元素并非我们所想访问的元素,而元素的 zval.u2.next 保存的值正是另一个具有相同散列值的元素对应 arData 数组的下标,所以我们可以不断通过 zval.u2.next 的值遍历直到找到键名相同的元素。

扩容

PHP 的数组在底层实现了自动扩容机制,当插入一个元素且没有空闲空间时,就会触发自动扩容机制,扩容后再执行插入。

扩容的过程为:

如果已删除元素所占比例达到阈值,则会移除已被逻辑删除的 Bucket,然后将后面的 Bucket 向前补上空缺的 Bucket,因为 Bucket 的下标发生了变动,所以还需要更改每个元素在中间映射表中储存的实际下标值。

如果未达到阈值,PHP 则会申请一个大小是原数组两倍的新数组,并将旧数组中的数据复制到新数组中,因为数组长度发生了改变,所以 key-value 的映射关系需要重新计算,这个步骤为重建索引。

重建散列表

在删除某一个数组元素时,会先使用标志位对该元素进行逻辑删除,即在删除 value 时只是将 value 的 type 设置为 IS_UNDEF,而不会立即删除该元素所在的 Bucket,因为如果每次删除元素立刻删除 Bucket 的话,每次都需要进行排列操作,会造成不必要的性能开销。

所以,当删除元素达到一定数量或扩容后都需要重建散列表,即移除被标记为删除的 value。因为 value 在 Bucket 位置移动了或哈希数组 nTableSize 变化了导致 key 与 value 的映射关系改变,重建过程就是遍历 Bucket 数组中的 value,然后重新计算映射值更新到散列表。

关于 PHP7 的数组底层实现就总结这么些了,因为水平有限也无法研究的十分详尽清楚,如果有疑问或者不足之处欢迎提出~~

参考资料

《PHP7 的底层设计与源码实现》

php7-internal

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

PHP 相关文章推荐
php中的实现trim函数代码
Mar 19 PHP
php中去除所有js,html,css代码
Oct 12 PHP
php程序的国际化实现方法(利用gettext)
Aug 14 PHP
php发送与接收流文件的方法
Feb 11 PHP
php根据指定位置和长度获得子字符串的方法
Mar 17 PHP
PHP中的类型约束介绍
May 11 PHP
基于ThinkPHP实现批量删除
Dec 18 PHP
php获取一定范围内取N个不重复的随机数
May 28 PHP
PHP Cookie学习笔记
Aug 23 PHP
Yii实现复选框批量操作实例代码
Mar 15 PHP
PHP基于ORM方式操作MySQL数据库实例
Jun 21 PHP
解决安装WampServer时提示缺少msvcr110.dll文件的问题
Jul 09 PHP
PHP实现cookie跨域session共享的方法分析
Aug 23 #PHP
php常用经典函数集锦【数组、字符串、栈、队列、排序等】
Aug 23 #PHP
php中错误处理操作实例分析
Aug 23 #PHP
php+js实现的无刷新下载文件功能示例
Aug 23 #PHP
php简单检测404页面的方法示例
Aug 23 #PHP
PHP Redis扩展无法加载的问题解决方法
Aug 22 #PHP
PHP Primary script unknown 解决方法总结
Aug 22 #PHP
You might like
Protoss魔法科技
2020/03/14 星际争霸
用PHP+java实现自动新闻滚动窗口
2006/10/09 PHP
php利用iframe实现无刷新文件上传功能的代码
2011/09/29 PHP
linux使用crontab实现PHP执行计划定时任务
2014/05/10 PHP
php从身份证获取性别和出生年月
2017/02/09 PHP
jquery text,radio,checkbox,select操作实现代码
2009/07/09 Javascript
JS运行耗时操作的延时显示方法
2010/11/19 Javascript
调用jQuery滑出效果时闪烁的解决方法
2014/03/27 Javascript
nw.js实现类似微信的聊天软件
2015/03/16 Javascript
日常收集整理的JavaScript常用函数方法
2015/12/10 Javascript
JS密码生成与强度检测完整实例(附demo源码下载)
2016/04/06 Javascript
js从外部获取图片的实现方法
2016/08/05 Javascript
jQuery布局组件EasyUI Layout使用方法详解
2017/02/28 Javascript
JavaScript门面模式详解
2017/10/19 Javascript
React Native 搭建开发环境的方法步骤
2017/10/30 Javascript
微信小程序input框中加入小图标的实现方法
2018/06/19 Javascript
推荐15个最好用的JavaScript代码压缩工具
2019/02/13 Javascript
JavaScript中如何对多维数组(矩阵)去重的实现
2019/12/04 Javascript
vue仿淘宝滑动验证码功能(样式模仿)
2019/12/10 Javascript
JavaScript实现商品评价五星好评
2020/11/30 Javascript
[01:33]一分钟玩转DOTA2第三弹:DOTA2&DotA快捷操作大对比
2014/06/04 DOTA
windows系统中python使用rar命令压缩多个文件夹示例
2014/05/06 Python
Python中优化NumPy包使用性能的教程
2015/04/23 Python
python脚本监控docker容器
2016/04/27 Python
Python之os操作方法(详解)
2017/06/15 Python
解决python大批量读写.doc文件的问题
2018/05/08 Python
利用CSS3的border-radius绘制太极及爱心图案示例
2016/05/17 HTML / CSS
全球性的奢侈品梦工厂:Forzieri(福喜利)
2019/02/20 全球购物
Shell编程面试题
2012/05/30 面试题
计算机应用专业应届毕业生中文求职信范文
2013/11/29 职场文书
房地产还款计划书
2014/01/10 职场文书
运动会稿件100字
2014/09/24 职场文书
学生穿着不得体检讨书
2014/10/12 职场文书
2015年学校教科室工作总结
2015/07/20 职场文书
应届毕业生的自我评价
2019/06/21 职场文书
CSS3 制作精美的定价表
2021/04/06 HTML / CSS