首页 » Web前端 » php内核分析技巧_深入理解 PHP7 内核之 HashTable

php内核分析技巧_深入理解 PHP7 内核之 HashTable

访客 2024-10-25 0

扫一扫用手机浏览

文章目录 [+]

在PHP5的实现中, Hashtable的核心是存储了一个个指向zval指针的指针, 也便是zval(我碰着不少的同学问为什么是zval, 而不是zval, 这个缘故原由实在很大略, 由于Hashtable中的多个位置都可能指向同一个zval, 那么最常见的一个可能便是在COW的时候, 当我们须要把一个变量指向一个新的zval的时候, 如果在符号表中存的是zval, 那们我们就做不到对一处修正, 所有的持有方都有感知, 以是必须是zval), 这样的设计在最初的出发点是为了让Hashtable可以存储任何尺寸的任何信息, 不仅仅是指针, 还可以存储一段内存值(当然实际上大部分情形下,比如符号表还是存的zval的指针)。

PHP5的代码中也用了比较Hack的办法来判断存储的是什么:

php内核分析技巧_深入理解 PHP7 内核之 HashTable

它来判断存储的size是不是一个指针大小, 从而采取不同的办法来更新存储的内容。
非常Hack的办法。

php内核分析技巧_深入理解 PHP7 内核之 HashTable
(图片来自网络侵删)

PHP5的Hashtable对付每一个Bucket都是分开申请开释的。

而存储在Hashtable中的数据是也会通过pListNext指针串成一个list,可以直接遍历,

问题

在写PHP7的时候,我们详细思考了几点可能优化的点,包括也从性能角度总结了以下目前这种实现的几个问题:

Hashtable在PHP中,运用最多的是保存各种zval, 而PHP5的HashTable设计的太通用,可以设计为专门为了存储zval而优化, 从而能减少内存占用。
2. 缓存局部性问题, 由于PHP5的Hashtable的Bucket,包括zval都是独立分配的,并且采取了List来串Hashtable中的所有元素,会导致在遍历或者顺序访问一个数组的时候,缓存不友好。
比如如图所示在PHP代码中常见的foreach一个数组, 就会发生多次的内存跳跃.3. 和1类似,在PHP5的Hashtable中,要访问一个zval,由于是zval, 那须要至少解指针俩次,一方面是缓存不友好,其余一方面也是效率低下。
比如上图中,蓝色框的部分,我们找到数组中的bucket往后,还须要解开zval,才可以读取到实际的zval的内容。
也便是须要发生俩次内存读取。
效率低下。

当然还有很多的其他的问题,此处不再赘述,说实在的毕竟俩年多了,当时怎么想的,现在有些也想不起来了, 现在我们来看看PHP7的

PHP7

首先在PHP7中,我们当时的考虑是可能由于担心Hashtable用的太多,我们新设计的构造体可能不一定能Cover所有的场景,于是我们新定义了一个构造体叫做zend_array, 当然末了经由一系列的努力,创造zend_array可以完备替代Hashtable, 末了还是保留了Hashtable和zend_array俩个名字,只不过互为Alias.再下面的文章中,我会用HashTable来特指PHP5中的Hashtable,而用zend_array来指代PHP7中的Hashtable.

我们先来看看zend_array的定义:

比较PHP5时期的Hashtable,zend_array的内存占用从PHP5点72个字节,降落到了56个字节,想想一个PHP生命进程中成千上万的数组,内存降落明显。

此处特殊解释下ZEND_ENDIAN_LOHT_4这个浸染,这个是为理解决大小端问题,在大端小端上都能让个中的元素担保同样的内存存储顺序,可以方便我们写出通用的位操作。
在PHP7种,位操作运用的很多,由于这样一来一个字节就可以保存8个状态位, 很节省内存:

而数据会核心保存在arData中,arData是一个Bucket数组,Bucket定义:

再比拟看下PHP5多Bucket:

内存占用从72字节,降落到了32个字节,想想一个PHP进程中几十万的数组元素,这个内存降落就更明显了.

比拟的看,

现在的冲突拉链被bauck.zval->u2.next替代, 于是bucket->pNext, bucket->pLast可以去掉了。
zend_array->arData是一个数组,以是也就不再须要pListNext, pListLast来保持顺序了, 他们也可以去掉了。
现在数组中元素的先后顺序,完备根据它在arData中的index顺序决定,先加入的元素在低的index中。
PHP7中的Bucket现在直接保存一个zval, 取代了PHP5时期bucket中的pData和pDataPtr。
末了便是PHP7中现在利用zend_string作为数组的字符串key,取代了PHP5时期bucket的key, nKeylength.

现在我们来整体看下zend_array的组织图:

回顾下深入理解PHP7内核之ZVAL, 现在的zend_array就可以搪塞各种场景下的HashTable的浸染了。

特殊解释对是除了一个问题便是之条件到过的IS_INDIRECT, 不知道大家是否还记得. 上一篇我说过原来HashTable为什么要设计保存zval, 那么现在由于_Bucket直接保存的是zval了,那怎么办理COW的时候一处修正多处可见的需求呢?IS_INDIRECT就运用而生了,IS_INDIRECT类型实在可以理解为便是一个zval的构造体。
它被广泛运用在GLOBALS,Properties等多个须要俩个HashTable指向同于一个ZVAL的场景。

其余,对付原来一些扩展会利用HashTable来保存一些自己的内存,现在可以通过IS_PTR这种zval类型来实现。

现在arData由于是一个连续的数组了,那么当foreach的时候,就可以顺序访问一块连续的内存,而现在zval直接保存在bucket中,那么在绝大部分情形下(不须要外部指针的内容,比如long,bool之类的)就可以不须要任何额外的zval指针解引用了,缓存局部性友好,性能提升非常明显。

还有便是PHP5的时期,查找数组元素的时候,由于通报进来的是char key,所有须要每次查找都打算key的Hash值,而现在查找的时候通报进来的key是zend_string, Hash值不须要重新打算,此处也有部分性能提升。

当然,PHP7也保留了直接通过char 查找的zend_hash_str_find API,这对付一些只有char的场景,可以避免天生zend_string的内存开销,也是很有用的。

其余,我们还做了不少进一步的优化:

Packed array

对付字符串key的数组来说, zend_array在arHash中保存了Hash值到arData的对应,有同学可能会好奇怎么没有在zend_array中看到arHash? 这是由于arHash和arData是一次分配的:

如图,事实上arData是一块分配的内存的中间部分,分配的内存真正的起始位置实在是pointer,而arData是打算过的一处中间位置,这样就可以用一个指针来表达俩个位置,分别通过前后偏移来获取位置, 比如-1对应的是arHash[0], 这个技巧在PHP7的过程中我们也大量运用了,比如由于zend_object是变长的,以是不能在他后面有其他元素,为了实现一些自定义的object,那么我们会在zend_object前面分配自定义的元素等等。

而对付全部是数字key的数组,arHash就显得没那么必要了,以是此时我们就用了一种新的数组, packed array来优化这个场景。

对付HASH_FLAG_PACKED的数组(标志在zend_array->u.flags)中,它们是只有连续数字key的数组,它们不须要Hash值来映射,以是这样的数组读取的时候,就相称于你直接访问C数组,直接根据偏移来获取zval.

如图所示的大略测试,在我的机器上输出如下(请把稳,这个测试的部分结果可能会受你的机器,包括装了什么扩展干系,以是记得要-n):

可以看到, 当我们利用$array[“foo”]=1, 强制一个数组从PACKED ARRAY变成一个Mixed Array往后,内存增长很明显,这部分是由于须要为10000个arHash分配内存。

而通过Index遍历的速率,Packed Array仅仅是Mixed Array的78%。

Static key array

对付字符串array来说, destructor的时候是须要开释字符串key的, 数组copy的时候也要增加key的计数,但是如果所有的key都是INTERNED字符串,那事实上我们不须要管这些了。
于是就有了这个HASH_FLAG_STATIC_KEYS。

Empty array

我们剖析创造,在实际利用中,有大量的空数组,针对这些, 我们在初始化数组的时候,如果不分外申明,默认是不会分配arData的,此时会把数组标志为HASH_FLAG_UNINITIALIZED, 只有当发生实际的写入的时候,才会分配arData。

Immutable array

类似于INTERNED STRING,PHP7中我们也引入了一种Imuutable array, 标志在array->gc.flags中的IS_ARRAY_IMMUTABLE, 大家可以理解为不可变动的数组,对付这种数组,不会发生COW,不须要计数,这个也会极大的提高这种数据的操作性能,我的Yaconf中也大量运用了这种数据特性。

SIMD

后来的PHP7的版本中,我实现了一套SIMD指令集优化的框架,比如SIMD的base64_encode, 而在HashTable的初始化中,我们也运用了部分这样的指令集(此处运用虽然很眇小,但有必要提一下):

存在的问题

在实现zend_array更换HashTable中我们碰着了很多的问题,绝大部份它们都被办理了,但遗留了一个问题,由于现在arData是连续分配的,那么当数组增长大小到须要扩容到时候,我们只能重新realloc内存,但系统并不担保你realloc往后,地址不会发生变革,那么就有可能:

比如上面的例子, 首先是一个全局数组,然后在函数crash中, 在+= opcode handler中,zend vm会首先获取array[0]的内容,然后+$var, 但var是undefined variable, 以是此时会触发一个未定义变量的notice,而同时我们设置了error_handler, 在个中我们给这个数组增加了一个元素, 由于PHP中的数组按照2^n的空间预先申请,此时数组满了,须要resize,于是发生了realloc,从error_handler返回往后,array[0]指向的内存就可能发生了变革,此时会涌现内存读写缺点,乃至segfault,有兴趣的同学,可以考试测验用valgrind跑这个例子看看。

但这个问题的触发条件比较多,修复须要额外对数据构造,或者须要拆分add_assign对性能会有影响,其余绝大部分情形下由于数组的预先分配策略存在,以及其他大部分多opcode handler读写操作基本都很附近,这个问题实在很难被实际代码触发,以是这个问题一贯悬停着。

须要软考资料的同学可以私信我哦

更多精彩内容请点击“理解更多”

标签:

相关文章

房山第一探寻历史文化名区的魅力与发展

房山区,位于北京市西南部,历史悠久,文化底蕴深厚。作为北京市的一个重要组成部分,房山区的发展始终与首都的发展紧密相连。房山区积极推...

Web前端 2025-02-18 阅读1 评论0

手机话费开钻代码数字时代的便捷生活

我们的生活越来越离不开手机。手机话费作为手机使用过程中的重要组成部分,其充值方式也在不断创新。手机话费开钻代码应运而生,为用户提供...

Web前端 2025-02-18 阅读1 评论0

探寻专业奥秘如何查询自己专业的代码

计算机科学已成为当今社会不可或缺的一部分。掌握一门专业代码对于个人发展具有重要意义。面对繁杂的学科体系,如何查询自己专业的代码成为...

Web前端 2025-02-18 阅读1 评论0