当前位置: 首页 > news >正文

tipi 深入理解php内核 pdf_深入理解 PHP7 内核之 HashTable

(给PHP开发者加星标,提升PHP技能)

转自:鸟哥/风雪之隅

www.laruence.com/2020/02/25/3182.html

之前俩篇文章《深入理解 PHP7 内核之 zval》和《深入理解 PHP7 内核之 Reference》 ,我介绍了当时在开发PHP7的时候对zval和reference的一些改造思考和结果, 之后因为确实精力有限就没有继续往下写,时隔一年多以后,因为这场突如其来的疫情,在家办公的时间很多, 于是终于有了时间让我来继续介绍一下PHP7的中Hashtable的变化, 以及当时我们做这些变化背后的考量.

PHP5

对于 PHP 内核一直有关注的同学, 应该对 PHP5 的 Hashtable 会比较熟悉, 但我们还是先来简单回顾一下PHP5的Hashtable:6f3e83358400177825774c479ceab5db.png在PHP5的实现中, Hashtable的核心是存储了一个个指向zval指针的指针, 也就是zval**(我遇到不少的同学问为什么是zval**, 而不是zval*, 这个原因其实很简单, 因为Hashtable中的多个位置都可能指向同一个zval, 那么最常见的一个可能就是在COW的时候, 当我们需要把一个变量指向一个新的zval的时候, 如果在符号表中存的是zval*, 那们我们就做不到对一处修改, 所有的持有方都有感知, 所以必须是zval**), 这样的设计在最初的出发点是为了让Hashtable可以存储任何尺寸的任何信息, 不仅仅是指针, 还可以存储一段内存值(当然实际上大部分情况下,比如符号表还是存的zval的指针)。

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

#define UPDATE_DATA(ht, p, pData, nDataSize)                                                if (nDataSize == sizeof(void*)) {                                                           if ((p)->pData != &(p)->pDataPtr) {                                                         pefree_rel((p)->pData, (ht)->persistent);                                           }                                                                                       memcpy(&(p)->pDataPtr, pData, sizeof(void *));                                          (p)->pData = &(p)->pDataPtr;                                                        } else {                                                                                    if ((p)->pData == &(p)->pDataPtr) {                                                         (p)->pData = (void *) pemalloc_rel(nDataSize, (ht)->persistent);                        (p)->pDataPtr=NULL;                                                                 } else {                                                                                    (p)->pData = (void *) perealloc_rel((p)->pData, nDataSize, (ht)->persistent);   \            /* (p)->pDataPtr is already NULL so no need to initialize it */             \        }                                                                                      memcpy((p)->pData, pData, nDataSize);                                               }

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

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

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

问题

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

  • Hashtable在PHP中,应用最多的是保存各种zval, 而PHP5的HashTable设计的太通用,可以设计为专门为了存储zval而优化, 从而能减少内存占用。

  • 2. 缓存局部性问题, 因为PHP5的Hashtable的Bucket,包括zval都是独立分配的,并且采用了List来串Hashtable中的所有元素,会导致在遍历或者顺序访问一个数组的时候,缓存不友好。83cecf33ee57eae50ee6c03ed60f0608.png比如如图所示在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的定义:

struct _zend_array {    zend_refcounted_h gc;    union {        struct {            ZEND_ENDIAN_LOHI_4(                zend_uchar    flags,                zend_uchar    _unused,                zend_uchar    nIteratorsCount,                zend_uchar    _unused2)        } 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;};

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

此处特别说明下ZEND_ENDIAN_LOHT_4这个作用,这个是为了解决大小端问题,在大端小端上都能让其中的元素保证同样的内存存储顺序,可以方便我们写出通用的位操作。在PHP7种,位操作应用的很多,因为这样一来一个字节就可以保存8个状态位, 很节省内存:)

#ifdef WORDS_BIGENDIAN# define ZEND_ENDIAN_LOHI_4(a, b, c, d)    d; c; b; a;#else# define ZEND_ENDIAN_LOHI_4(a, b, c, d)    a; b; c; d;#endif

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

typedef struct _Bucket {    zval              val;    zend_ulong        h;   /* hash value (or numeric index)   */    zend_string      *key;              /* string key or NULL for numerics */} Bucket

再对比看下PHP5多Bucket:

typedef struct bucket {    ulong h;                        /* Used for numeric indexing */    uint nKeyLength;    void *pData;    void *pDataPtr;    struct bucket *pListNext;    struct bucket *pListLast;    struct bucket *pNext;    struct bucket *pLast;    const char *arKey;} 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的组织图:2ec171beca1d687c275e11159d92f486.png

回顾下深入理解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指针解引用了,缓存局部性友好,性能提升非常明显。

339d46515b39cfe018df710077b01917.png

还有就是PHP5的时代,查找数组元素的时候,因为传递进来的是char *key,所有需要每次查找都计算key的Hash值,而现在查找的时候传递进来的key是zend_string, Hash值不需要重新计算,此处也有部分性能提升。

ZEND_API zval* ZEND_FASTCALL zend_hash_find(const HashTable *ht, zend_string *key);ZEND_API zval* ZEND_FASTCALL zend_hash_str_find(const HashTable *ht, const char *key, size_t len);ZEND_API zval* ZEND_FASTCALL zend_hash_index_find(const HashTable *ht, zend_ulong h);ZEND_API zval* ZEND_FASTCALL _zend_hash_index_find(const HashTable *ht, zend_ulong h);

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

另外,我们还做了不少进一步的优化:

Packed array

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

HashTable Data Layout=====================         +=============================+pointer->| HT_HASH(ht, ht->nTableMask) |         | ...                         |         | HT_HASH(ht, -1)             |         +-----------------------------+arData ->| Bucket[0]                   |         | ...                         |         | Bucket[ht->nTableSize-1]    |         +=============================+

如图,事实上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.

<?php echo "Packed array:\n";$begin = memory_get_usage();$array = range(0, 10000);echo "Memory: ", memory_get_usage() - $begin, " bytes\n";$begin = memory_get_usage();$array[10001] = 1;echo "Memory Increased: ", memory_get_usage() - $begin, " bytes\n";$start = microtime(true);for ($i = 0; $i < 10000; $i++) {    $array[$i];}echo "Time: ", (microtime(true) - $start) * 1000 , " ms\n";unset($array);echo "\nMixed array:\n";$begin = memory_get_usage();$array = range(0, 10000);echo "Memory: ", memory_get_usage() - $begin, " bytes\n";$begin = memory_get_usage();$array["foo"] = 1;echo "Memory Increased: ", memory_get_usage() - $begin, " bytes\n";$start = microtime(true);for ($i = 0; $i < 10000; $i++) {    $array[$i];}echo "Time: ", (microtime(true) - $start) * 1000 ," ms\n";

如图所示的简单测试,在我的机器上输出如下(请注意,这个测试的部分结果可能会受你的机器,包括装了什么扩展相关,所以记得要-n):

$ /home/huixinchen/local/php74/bin/php -n /tmp/1.phpPacked array:Memory: 528480 bytesMemory Increased: 0 bytesTime: 0.49519538879395 msMixed array:Memory: 528480 bytesMemory Increased: 131072 bytesTime: 0.63300132751465 ms

可以看到, 当我们使用$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的初始化中,我们也应用了部分这样的指令集(此处应用虽然很微小,但有必要提一下):

ifdef __SSE2__        do {            __m128i xmm0 = _mm_setzero_si128();            xmm0 = _mm_cmpeq_epi8(xmm0, xmm0);            _mm_storeu_si128((__m128i*)&HT_HASH_EX(data,  0), xmm0);            _mm_storeu_si128((__m128i*)&HT_HASH_EX(data,  4), xmm0);            _mm_storeu_si128((__m128i*)&HT_HASH_EX(data,  8), xmm0);            _mm_storeu_si128((__m128i*)&HT_HASH_EX(data, 12), xmm0);        } while (0);#else        HT_HASH_EX(data,  0) = -1;        HT_HASH_EX(data,  1) = -1;        HT_HASH_EX(data,  2) = -1;        HT_HASH_EX(data,  3) = -1;        HT_HASH_EX(data,  4) = -1;        HT_HASH_EX(data,  5) = -1;        HT_HASH_EX(data,  6) = -1;        HT_HASH_EX(data,  7) = -1;        HT_HASH_EX(data,  8) = -1;        HT_HASH_EX(data,  9) = -1;        HT_HASH_EX(data, 10) = -1;        HT_HASH_EX(data, 11) = -1;        HT_HASH_EX(data, 12) = -1;        HT_HASH_EX(data, 13) = -1;        HT_HASH_EX(data, 14) = -1;        HT_HASH_EX(data, 15) = -1;#endif

存在的问题

在实现zend_array替换HashTable中我们遇到了很多的问题,绝大部份它们都被解决了,但遗留了一个问题,因为现在arData是连续分配的,那么当数组增长大小到需要扩容到时候,我们只能重新realloc内存,但系统并不保证你realloc以后,地址不会发生变化,那么就有可能:

<?php $array = range(0, 7);set_error_handler(function($err, $msg) {    global $array;    $array[] = 1; //force resize;});function crash() {    global $array;    $array[0] += $var; //undefined notice}crash();

比如上面的例子, 首先是一个全局数组,然后在函数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读写操作基本都很临近,这个问题其实很难被实际代码触发,所以这个问题一直悬停着。

最后,就暂时写到这里吧,以后想到再补充吧。 另外这里的大部分的内容,你也可以从我4年前的一个演讲ppt: The Secret of PHP7’s Performance 中找到。

推荐阅读   点击标题可跳转 深入理解 PHP7 内核之 zval ; 深入理解 PHP7 内核之 Reference

看完本文有收获?请分享给更多人

关注「PHP开发者」加星标,提升PHP技能

1897686ec5c90b01ed893bfc09728b15.png

好文章,我在看❤️

相关文章:

  • ps cs6 磨皮插件_Portraiture 3 for mac(ps磨皮滤镜插件) v3.5.4(3540)版
  • python index函数是左闭右开吗_Python容器类型公共方法总结
  • flutter 图表_Flutter 与 Chrome OS 珠联璧合
  • 计算加减乘除混合运算python实现_python,实现计算器程序,加减乘除混合运算加括号,完善实现...
  • 页面布局让footer居页面底部_网站页面底部固定的方法
  • ocr语种识别_【梦想云中台能力】智能图片处理OCR
  • fifo算法_LRU缓存算法的实现
  • 安卓实训项目源码_【兼职项目】预算3万开发无线温度电流传感,2万开发直流电机打磨机控制...
  • hbuilderx内置服务器启动失败_安卓应用perfect player竟然内置直播源高速观看港台卫视/影视/动画...
  • git rm -r --cached_git 撤销对文件的追踪
  • hadoop生态_大数据运营技术与工具:Hadoop生态系统
  • erp系统服务器托管_勤哲Excel服务器做门业生产企业管理ERP系统
  • python3爬虫框架scrapy_Python3环境安装Scrapy爬虫框架过程及常见错误
  • 廖雪峰python教程整理_廖雪峰老师Python3教程练习整理
  • gif图片生成器_Python几行代码制作Gif动图
  • [笔记] php常见简单功能及函数
  • 【翻译】Mashape是如何管理15000个API和微服务的(三)
  • canvas 绘制双线技巧
  • ComponentOne 2017 V2版本正式发布
  • Kibana配置logstash,报表一体化
  • thinkphp5.1 easywechat4 微信第三方开放平台
  • vue 个人积累(使用工具,组件)
  • Vue.js源码(2):初探List Rendering
  • vue-loader 源码解析系列之 selector
  • Web设计流程优化:网页效果图设计新思路
  • 阿里云爬虫风险管理产品商业化,为云端流量保驾护航
  • 反思总结然后整装待发
  • 分布式任务队列Celery
  • 关于 Linux 进程的 UID、EUID、GID 和 EGID
  • 通过git安装npm私有模块
  • 用Node EJS写一个爬虫脚本每天定时给心爱的她发一封暖心邮件
  • 优化 Vue 项目编译文件大小
  • 阿里云ACE认证学习知识点梳理
  • 从如何停掉 Promise 链说起
  • 摩拜创始人胡玮炜也彻底离开了,共享单车行业还有未来吗? ...
  • ​​​​​​​sokit v1.3抓手机应用socket数据包: Socket是传输控制层协议,WebSocket是应用层协议。
  • ​html.parser --- 简单的 HTML 和 XHTML 解析器​
  • ​io --- 处理流的核心工具​
  • $(document).ready(function(){}), $().ready(function(){})和$(function(){})三者区别
  • (32位汇编 五)mov/add/sub/and/or/xor/not
  • (function(){})()的分步解析
  • (教学思路 C#之类三)方法参数类型(ref、out、parmas)
  • (九)One-Wire总线-DS18B20
  • (考研湖科大教书匠计算机网络)第一章概述-第五节1:计算机网络体系结构之分层思想和举例
  • (删)Java线程同步实现一:synchronzied和wait()/notify()
  • (十七)Flask之大型项目目录结构示例【二扣蓝图】
  • (五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境
  • (转)IIS6 ASP 0251超过响应缓冲区限制错误的解决方法
  • (转)大型网站的系统架构
  • (转)原始图像数据和PDF中的图像数据
  • *p++,*(p++),*++p,(*p)++区别?
  • . NET自动找可写目录
  • ./configure、make、make install 命令
  • .FileZilla的使用和主动模式被动模式介绍
  • .net core 依赖注入的基本用发