PHP7的性能优化
# php7性能优化
2015年,PHP7的发布可以说是在技术圈里引起了不小的轰动,因为它的执行效率比PHP5直接翻了一倍。本人写过五六年PHP,三年python,三年Java,感觉现在的PHP性能真不慢,开发效率也很高。 开发效率比Java高太多了。性能比python也强很多。曾经压测go框架Gin和swoole,性能都差不多。当然只是做了一个很简单的接口,go还是一门相当不错的语言。下面来探探php7优化的一些思想。
# php7的zval的变化
- 存储变量的结构体变小,结构体成员尽量公用内存,内存占用降低,php7⼀个变量实际占⽤的内存⼤⼩为8字节,php5占用48字节,操作变快
- php5是通过MAKE_STD_ZVAL动态的从堆内存上分配一个zval内存,php7直接使用栈内存,少了一次内存分配,php在大量创建变量时,php7会在栈上预分配一块内存来存放这些zval,栈内存可直接读取,堆内存不可以,节省了大量的内存分配和管理操作。
php5.3中的zval
在64位操作系统下,该zval_struct结构体中的由四个成员构成,其中zvalue_value稍微复杂一些,是一个联合体。联合体中最长的成员是一个指针加一个int,8+4=12字节。但是默认情况下,会进行内存对齐,故zval_struct会占用16字节。那么。
_zval_struct总的字节 = value(16)+ refcount__gc(4)+ type(1)+ is_ref__gc(1)= 占用22字节。
最后再考虑下内存对齐,实际占用24字节。
php7中的zval
7.2中的zval_struct结构体里由3个成员构成,其中zend_value看起来比较复杂,实际上只是一个8字节的联合体。u1也是一个联合体,占用是4个字节。u2也一样。这样zval_struct就实际占用16个字节。
# PHP7 数据存储的变化
hash计算:PHP底层对于字符串、数组、类属性、类方法、函数,访问时都要先通过hashtable查找到对应的指针,再执行对应的操作
- PHP7为字符串单独创建了新类型叫做zend_string,除了char *指针和长度之外,增加了一个hash字段,用于保存字符串的hash值。
- array查询有大量的$array[$key],大部分情况下$key的值都是不变的,PHP7将hash值保存起来,节省了大量的hash计算。
- 数组元素与hash映射表共享内存,降低了内存空间的占用。
# PHP7 hashtable 存储变化
PHP5的链表是物理上的链表,链表中bucket之间的上下游关系通过真实存在的指针维护。 PHP7的链表是⼀种逻辑上的链表,所有bucket都分配在连续的数组内存中,不再通过指针维护上下游关系,每⼀个bucket只维护下⼀个bucket在数组中的索引(因为是连续内存,通过索引可以快速定位到bucket),即可完成链表上bucket的遍历。 哈希冲突:哈希冲突一般用链地址法或开放寻址法,PHP5和PHP7都使用链地址法解决哈希冲突,因为本来就是维护的哈希链表。 hashtable桶内直接存放数据,减少了内存申请次数,顺便也提升了cache命中率和访问速度。php7的arData就是一块大内存。
在5.3里HashTable就是一个大struct,
uint nTableSize 4字节
uint nTableMask 4字节
uint nNumOfElements 4字节,
ulong nNextFreeElement 8字节 注意这前面的4个字节会被浪费掉,因为nNextFreeElement的开始地址需要对齐
Bucket *pInternalPointer 8字节
Bucket *pListHead 8字节
Bucket *pListTail 8字节
Bucket **arBuckets 8字节
dtor_func_t pDestructor 8字节
zend_bool persistent 1字节
unsigned char nApplyCoun 1字节
zend_bool bApplyProtection 1字节
最终,总字节数 = 4+4+4+4(nNextFreeElement前面这四个字节会留空)+8+8+8+8+8+8+1+1+1 = 67字节。再加上结构体本身要对齐到8的整数倍,所以实际占用72字节。
在7.2里HashTable
zend_refcounted_h gc 看起来唬人,实际就是个long,占用8字节
union... u 占用4字节
uint32_t 占用4字节
Bucket* 指针占用8字节
uint32_t nNumUsed 占用4字节
uint32_t nNumOfElements 占用4字节
uint32_t nTableSize 占用4字节
uint32_t nInternalPointer 占用4字节
zend_long nNextFreeElement 占用8字节
dtor_func_t pDestructor 占用8字节
总占用
字节数 = 8+4+4+8+4+4+4+4+8+8 = 56字节,并且正好达到了内存对齐的状态,没有额外的浪费。
另外还有PHP源代码里经常出镜的Buckets也从72下降到了32字节了。
# 函数调用
- 改进了函数的调用机制,通过对参数传递环节的优化,减少一些指令操作,提高了执行效
- PHP程序中会大量使用call_user_function, is_int/string/array, strlen , defined 函数。PHP5 都是以扩展函数的方式提供,PHP7中这4类函数改成ZendVM的OPCODE指令,执行更快。
# 思路分析
从上面两个数据结构来看,都有很大的变化,从HashTable看,从72字节优化到了56字节,这内存节约20%多。在高性能上,每一字节内存都是非常珍贵的。Elasticsearch是一个非常出色的搜索引擎,研究过Elasticsearch就知道它在存储和查询的每个字节都设计的非常精妙。 个人从CPU层面分析下,有这两方面原因:
- CPU在向内存要数据的时候是以Cache Line为单位进行的,而Cache Line的大小就是64字节。回过头来看HashTable,在7.2里的56字节,只需要CPU向内存进行一次Cache Line大小的burst IO,就够了。而在5.3里的72字节,虽然只比Cache Line大了那么一丢丢,但是必须得进行两次burst IO才可以。所以,在计算机里,56字节相对72字节实际上是翻倍的性能提升!!
- CPU的L1、L2、L3的容量是固定的几十K或者几十M。假设Cache的都是HashTable,那么Cache容量不变的条件下,能Cache住的HashTable将会翻倍,缓存命中率提升一大截。要知道L1命中后只需要1ns多一点的耗时,而如果穿透到内存的话可能就需要40多纳秒的延时了,整整差了几十倍
最后,致敬PHP内核开发者,真的大牛,深谙CPU与内存的工作原理,表面上看起来只是几个字节的节约,但是实际上爆发出了巨大的性能提升!!
# zend_parse_parameters改为宏实现,性能提升15%
# AST(Abstract syntax tree)抽象语法树
PHP7 的内核中有一个重要的变化是加入了 AST(Abstract syntax tree)抽象语法树,指代码在计算机内存的一种树状数据结构,树上的每个节点都表示源代码中的一种结构,便于计算机理解和解析。
在 PHP5系列版本中,从 php 脚本到 opcodes 的执行的过程如下:
词法扫描分析(Lexing):将源文件转换成 token 流;
语法分析(Parsing):生成 op arrays。
PHP7 中在语法分析阶段先生成 AST:
词法扫描分析(Lexing):将源文件转换成 token 流。
语法分析(Parsing):从 token 流生成抽象语法树。
Compilation:从抽象语法树生成 op arrays。
这个表达式($a)['b'] = 1 就会被解析成下图这样的一棵树结构
# PHP8 引入JIT
PHP 8 引入了 JIT 编译器,这是一个重要的性能改进。JIT 编译器可以对一些高频执行的代码进行实时编译和优化,变成本地机器码,从而提高运行速度。根据 PHP 官方的测试数据,PHP 8 的性能比 PHP 7.4 提高了 10% 到 15%。这使得PHP8在处理大规模计算和性能要求较高的应用程序时更加强大。
C和C++就是将源代码编译然后生成二进制机器码去执行的,而php,python等脚本语言是将源代码转换成中间指令然后在vm(虚拟机)上执行,另外java系语言他们使用的JVM引擎底层也是JIT,是将java的字节码编译成二进制的机器码去执行的。对于计算密集型的的程序,JIT可以将PHP的opcode直接转换成机器码,可以大幅度提升PHP性能。