奥门新浦京官方网站PHP 性能分析与实验:性能的宏观分析

【编者按】此前,阅读过了很多关于 PHP
性能分析的文章,不过写的都是一条一条的规则,而且,这些规则并没有上下文,也没有明确的实验来体现出这些规则的优势,同时讨论的也侧重于一些语法要点。本文就改变 PHP
性能分析的角度,并通过实例来分析出 PHP 的性能方面需要注意和改进的点。

本文实例讲述了PHP中OpCode的原理。分享给大家供大家参考,具体如下:

前言

奥门新浦京官方网站 1

OpCode是一种PHP脚本编译后的中间语言,就像Java的ByteCode,或者.NET的MSL。
此文主要基于《 Understanding OPcode》和
网络,根据个人的理解和修改,特记录下来 :

鸟哥在博客中说,提高PHP 7性能的几个tips,第一条就是开启opcache:

对 PHP
性能的分析,我们从两个层面着手,把这篇文章也分成了两个部分,一个是宏观层面,所谓宏观层面,就是
PHP
语言本身和环境层面,一个是应用层面,就是语法和使用规则的层面,不过不仅探讨规则,更辅助以示例的分析。

PHP执行这段代码会经过如下4个步骤:

记得启用Zend Opcache,
因为PHP7即使不启用Opcache速度也比PHP-5.6启用了Opcache快,
所以之前测试时期就发生了有人一直没有启用Opcache的事情

宏观层面,也就是对 PHP 语言本身的性能分析又分为三个方面:

  1. Scanning ,将PHP代码转换为语言片段2. Parsing ,
    将Tokens转换成简单而有意义的表达式3. Compilation ,
    将表达式编译成Opocdes4. Execution ,
    顺次执行Opcodes,每次一条,从而实现PHP脚本的功能。

那么什么是Opcache呢?

  1. PHP 作为解释性语言性能有其天然的缺陷
  2. PHP 作为动态类型语言在性能上也有提升的空间
  3. 当下主流 PHP 版本本身语言引擎性能

注:现在有的Cache比如:APC ,可以使得PHP缓存Opcodes
,这样,每次有请求来临的时候,就不需要重复执行前面3步,从而能大幅的提高PHP的执行速度。

Opcache 的前生是 Optimizer+ ,它是PHP的官方公司 Zend
开发的一款闭源但可以免费使用的 PHP 优化加速组件。 Optimizer+
将PHP代码预编译生成的脚本文件 Opcode
缓存在共享内存中供以后反复使用,从而避免了从磁盘读取代码再次编译的时间消耗。同时,它还应用了一些代码优化模式,使得代码执行更快。从而加速PHP的执行。

一、PHP 作为解释性语言的性能分析与提升

PHP
作为一门脚本语言,也是解释性语言,是其天然性能受限的原因,因为同编译型语言在运行之前编译成二进制代码不同,解释性语言在每一次运行都面对原始脚本的输入、解析、编译,然后执行。如下是
PHP 作为解释性语言的执行过程。

奥门新浦京官方网站 2

如上所示,从上图可以看到,每一次运行,都需要经历三个解析、编译、运行三个过程。

那优化的点在哪里呢?可以想见,只要代码文件确定,解析到编译这一步都是确定的,因为文件已不再变化,而执行,则由于输入参数的不同而不同。在性能优化的世界里,至上绝招就是在获得同样结果的情况下,减少操作,这就是大名鼎鼎的缓存。缓存无处不在,缓存也是性能优化的杀手锏。于是乎
OpCode
缓存这一招就出现了,只有第一次需要解析和编译,而在后面的执行中,直接由脚本到
Opcode,从而实现了性能提速。执行流程如下图所示:

奥门新浦京官方网站 3

相对每一次解析、编译,读到脚本之后,直接从缓存读取字节码的效率会有大幅度的提升,提升幅度到底有多大呢?

我们来做一个没有 Opcode 缓存的实验。20 个并发,总共 10000 次请求没有经过
opcode 缓存的请求,,得到如下结果:

奥门新浦京官方网站 4

其次,我们在服务器上打开 Opcode 缓存。要想实现 opcode 缓存,只需要安装
APC、Zend OPCache、eAccelerator
扩展即可,即使安装了多个,也只启用其中一个。注意的是,修改了 php.ini
配置之后,需要重新加载 php-fpm 的配置。

这里分别启用 APC 和 Zend OPCache 做实验。启用 APC 的版本。

奥门新浦京官方网站 5

可以看到,速度有了较大幅度的提升,原来每个请求 110ms,每秒处理请求 182
个,启用了 APC 之后 68ms,每秒处理请求 294 个,提升速度将近 40%。

在启用了 Zend Opcache 的版本中,得到同 APC 大致相当的结果。每秒处理请求
291 个,每请求耗时 68.5ms。

奥门新浦京官方网站 6

从上面的这个实验可以看到,所用的测试页面,有 40ms
以上的时间花在了语法解析和编译这两项上。通过将这两个操作缓存,可以将这个处理过程的速度大大提升。

这里附加补充一下,OpCode 到底是什么东东,OpCode
编译之后的字节码,我们可以使用bytekit 这样的工具,或者使用 vld PHP
扩展来实现对 PHP 的代码编译。如下是 vld 插件解析代码的运行结果。

奥门新浦京官方网站 7

可以看到每一行代码被编译成相应的 OpCode 的输出。

首先,Zend/zend_language_scanner.c
会根据Zend/zend_language_scanner.l,来对输入的
PHP代码进行词法分析,从而得到一个一个的“词”,PHP4.2+开始提供了一个函数叫token_get_all
,这个函数就可以讲一段PHP代码 Scanning成Tokens;

 PHP的正常执行流程如下

二、PHP 作为动态类型语言的性能分析与改进

第二个是 PHP
语言是动态类型的语言,动态类型的语言本身由于涉及到在内存中的类型推断,比如在
PHP
中,两个整数相加,我们能得到整数值,一个整数和一个字符串相加,甚至两个字符串相加,都变成整数相加。而字符串和任何类型连接操作都成了字符串。

<?php
$a = 10.11;
$b = "30";
var_dump($a+$b);
var_dump("10"+$b);
var_dump(10+"20");
var_dump("10"+"20");

运行结果如下:

float(40.11)
int(40)
int(30)
int(30)

语言的动态类型为开发者提供了方便,语言本身则会因为动态类型而降低效率。在
Swift
中,有一个特性叫类型推断,我们可以看看类型推断会带来多大的一个效率上的差别呢?对于需要类型推断与不需要类型推断两段
Swift 代码,我们尝试编译一下看看效果如何。 第一段代码如下:

奥门新浦京官方网站 8

这是一段 Swift 代码,字典只有 14 个键值对,这段代码的编译,9
分钟了还没有编译完成(5G 内存,2.4GHz CPU),编译环境为 Swift 1.2,Xcode
6.4。

奥门新浦京官方网站 9

但是如果调整代码如下:

奥门新浦京官方网站 10

也就是加上了类型限定,避免了 planeLocation 的类型推断。编译过程花了 2S

奥门新浦京官方网站 11

可见,作为动态类型附加的类型推断操作极大地降低了程序的编译速度。
当然,这个例子有点极端,用 Swift 来类比 PHP 也不一定合适,因为 Swift
语言本身也还在不断的进化过程中。本例子只是表明在编程语言中,如果是动态类型语言,就涉及到对动态类型的处理,从编译的角度讲是会受影响的。

那么作为动态类型的 PHP 的效率如何提升呢?从 PHP
语言本身这个层面是没有办法解决的,因为你怎么写也是动态类型的代码。解决办法就是将PHP转化为静态类型的表示,也就是做成扩展,可以看到,鸟哥的很多项目,比如
Yaf 框架,都是做成了扩展的,当然这也是由于鸟哥是 C 高手。扩展由于是 C
或者 C++ 而写,所以不再是动态类型,又加之是编译好的,而 C
语言本身的效率也会提升很多。所以效率会大幅度提高。

下面我们来看一段代码,这段代码,只是实现了简单的素数运算,能计算指定值以内的素数个数,用的是普通的筛选法。现在看看扩展实现,跟
PHP
原生实现的效率差别,这个差别当然,不仅仅是动态类型和编译类型的差别,还有语言效率的差别。

首先是用纯 PHP 写成的算法,计算 1000 万以内的素数个数,耗时在 33s
上下,实验了三次,得到的结果基本相同。

奥门新浦京官方网站 12

其次,我们将这个求素数个数的过程,编写成了 PHP 扩展,在扩展中实现了
getprimenumbers
函数,输入一个整数,返回小于该整数的素数。得到的结果如下,这个效率的提升是非常惊人的,在
1.4s 上下即返回。速度提升 20 倍以上。

奥门新浦京官方网站 13

可以想见,静态和编译类型的语言,其效率得到了惊人的提升。本程序的 C
语言代码如下:

PHP_FUNCTION(get_prime_numbers)
{
    long value;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &value) == FAILURE) {
            return;
    }
     int *numbers = (int *)malloc(sizeof(int)*128*10000);
     memset(numbers, 0x0, 128*10000);
    int num = 2;
        numbers[0] = 2;
        numbers[1] = 3;
        bool flag = true;
        double f = 0;
        int i = 0;
        int j = 0;
        for(i=5; i<=value; i+=2)
        {
            flag = true;
            f = sqrt(i);
            for(j=0; j<num;j++)
            {
                if(i%numbers[j]==0)
                {
                    flag = false;
                    break;
                }
                if(numbers[j]>f)
                {
                    break;
                }
            }
            if(flag)
            { 
                numbers[num] = i;
                num++;
            }
        }
        free(numbers);
        RETURN_LONG(num);
}
');print_r;?>

Array( [0] => Array ( [0] => 367 [1] =>  1 ) [1] => Array ( [0] => 370 [1] => [2] => 2 ) [2] => Array ( [0] => 316 [1] => echo [2] => 2 ) [3] => Array ( [0] => 370 [1] => [2] => 2 ) [4] => Array ( [0] => 315 [1] => "Hello World" [2] => 2 ) [5] => ; [6] => Array ( [0] => 370 [1] => [2] => 2 ) [7] => Array ( [0] => 309 [1] => $a [2] => 3 ) [8] => Array ( [0] => 370 [1] => [2] => 3 ) [9] => = [10] => Array ( [0] => 370 [1] => [2] => 3 ) [11] => Array ( [0] => 305 [1] => 1 [2] => 3 ) [12] => Array ( [0] => 370 [1] => [2] => 3 ) [13] => + [14] => Array ( [0] => 370 [1] => [2] => 3 ) [15] => Array ( [0] => 305 [1] => 1 [2] => 3 ) [16] => ; [17] => Array ( [0] => 370 [1] => [2] => 3 ) [18] => Array ( [0] => 316 [1] => echo [2] => 4 ) [19] => Array ( [0] => 370 [1] => [2] => 4 ) [20] => Array ( [0] => 309 [1] => $a [2] => 4 ) [21] => ; [22] => Array ( [0] => 370 [1] => [2] => 4 ) [23] => Array ( [0] => 369 [1] => ?> [2] => 5 ))

奥门新浦京官方网站 14

三、PHP 语言本身底层性能引擎提升

第三个性能优化层面是语言本身的性能提升,这个就不是我们普通开发者所能做的了。在
PHP 7以前,寄希望于小版本的改进,但是改进幅度不是非常的显著,比如 PHP
5.3 、PHP 5.4、PHP 5.5、PHP 5.5
对同一段代码的性能比较,有一定程度的进步。

PHP 5.3 的版本在上面的例子中已讲过,需要 33s
左右的时间,我们现在来看别的PHP版本。分别运行如下:

PHP 5.4 版,相较 5.3 版已经有一定程度的提升。快 6 秒左右。

奥门新浦京官方网站 15

PHP 5.5 版在 PHP 5.4的基础上又进了一步,快了 6S。

奥门新浦京官方网站 16

PHP5.6 反而有些退步。

奥门新浦京官方网站 17

PHP 7 果真是效率提升惊人,是 PHP5.3 的 3 倍以上。

奥门新浦京官方网站 18

以上是求素数脚本在各个 PHP
版本之间的运行速度区别,尽管只测试了这一个程序,也不是特别的严谨,但是这是在同一台机器上,而且编译
configure 参数也基本一样,还是有一定可比性的。

在宏观层面,除了上面的这些之外,在实际的部署过程中,对 PHP
性能的优化,还体现为要减少在运行中所消耗的资源。所以 FastCGI 模式和
mod_php 的模式比传统的 CGI 模式也更为受欢迎。因为在传统的 CGI
模式中,在每一次脚本运行都需要加载所有的模块。而在程序运行完成了之后,也要释放模块资源。如下图所示:

奥门新浦京官方网站 19

而在 FastCGI 和 mod_php 模式中,则不需要如此。只有 php-fpm 或者 Apache
启动的时候,需要加载一次所有的模块,在具体的某次运行过程中,并不需要再次加载和释放相关的模块资源。

奥门新浦京官方网站 20

这样程序性能的效率得到了提升。以上就是有关 PHP
宏观层面的性能优化的分析,在本文的第二部分我们将探讨应用方面的 PHP
优化准则。敬请期待!

返回的结果,
源码中的字符串,字符,空格,都会原样返回。每个源代码中的字符,都会出现在相应的顺序处。而,其他的比如标签,操作符,语句,都会被转换成一个包含俩部分的Array:
Token ID
(也就是在Zend内部的改Token的对应码,比如,T_ECHO,T_STRING),和源码中的原来的内容。

 request请求(nginx,apache,cli等)–>Zend引擎读取.php文件–>扫描其词典和表达式
–>解析文件–>创建要执行的计算机代码(称为Opcode)–>最后执行Opcode–>
response 返回

接下来,就是Parsing阶段了,Parsing首先会丢弃Tokens
Array中的多于的空格,然后将剩余的Tokens转换成一个一个的简单的表达式

每一次请求PHP脚本都会执行一遍以上步骤,如果PHP源代码没有变化,那么Opcode也不会变化,显然没有必要每次都重新生成Opcode,结合在Web中无所不在的缓存机制,我们可以把Opcode缓存下来,以后直接访问缓存的Opcode岂不是更快,启用Opcode缓存之后的流程图如下所示:

  1. echo a constant string2. add two numbers together3. store the result
    of the prior expression to a variable4. echo a variable

奥门新浦京官方网站 21 

然后,就改Compilation阶段了,它会把Tokens编译成一个个op_array,每个op_arrayd包含如下5个部分:

 Opcode cache 的目地是避免重复编译,减少 CPU 和内存开销。

  1. Opcode数字的标识,指明了每个op_array的操作类型,比如add,echo2.
    结果存放Opcode结果3. 操作数1给Opcode的操作数4. 操作数25.
    扩展值1个整形用来区别被重载的操作符

下面介绍Opcache的安装

比如,PHP代码会被Parsing成:

安装:

[root@localhost html]# /usr/local/php/bin/php -dvld.active=1 hello.phpBranch analysis from position: 0Return foundfilename: /var/www/html/hello.phpfunction name: number of ops: 6compiled vars: !0 = $aline # op fetch ext return operands------------------------------------------------------------------------------- 2 0 ECHO 'Hello+world' 3 1 ADD ~0 1, 1 2 ASSIGN !0, ~0 4 3 ECHO !0 6 4 RETURN 1 5* ZEND_HANDLE_EXCEPTIONHello world2

1、找到opcache的扩展,我的是php7.1

每个操作数都是由以下两个部分组成:

yum list php71*

a) op_type : 为IS_CONST, IS_TMP_VAR, IS_VAR, IS_UNUSED, or IS_CV

2、安装扩展

b)
u,一个联合体,根据op_type的不同,分别用不同的类型保存了这个操作数的值

yum install php71w-opcache.x86_64

而对于var来说,每个var也不一样。 IS_TMP_VAR,
顾名思义,这个是一个临时变量
,保存一些op_array的结果,以便接下来的op_array使用,这种的操作数的u保存着一个指向变量表的一个句柄,这种操作数一般用~开头,比如~0,表示变量表的0号未知的临时变量IS_VAR
这种就是我们一般意义上的变量了,他们以$开头表示IS_CV
表示ZE2.1/PHP5.1以后的编译器使用的一种cache机制,这种变量保存着被它引用的变量的地址
,当一个变量第一次被引用的时候,就会被CV起来,以后对这个变量的引用就不需要再次去查找active符号表了,CV变量以
! 开头表示。

配置:

$a 变量就被优化成 !0 了。

zend_extension=opcache.so
[opcache]
;开启opcache
opcache.enable=1 

;CLI环境下,PHP启用OPcache
opcache.enable_cli=1

;OPcache共享内存存储大小,单位MB
opcache.memory_consumption=128 

;PHP使用了一种叫做字符串驻留(string
interning)的技术来改善性能。例如,如果你在代码中使用了1000次字符串“foobar”,在PHP内部只会在第一使用这个字符串的时候分配一个不可变的内存区域来存储这个字符串,其他的999次使用都会直接指向这个内存区域。这个选项则会把这个特性提升一个层次——默认情况下这个不可变的内存区域只会存在于单个php-fpm的进程中,如果设置了这个选项,那么它将会在所有的php-fpm进程中共享。在比较大的应用中,这可以非常有效地节约内存,提高应用的性能。
这个选项的值是以兆字节(megabytes)作为单位,如果把它设置为16,则表示16MB,默认是4MB
opcache.interned_strings_buffer=8

;这个选项用于控制内存中最多可以缓存多少个PHP文件。这个选项必须得设置得足够大,大于你的项目中的所有PHP文件的总和。
设置值取值范围最小值是 200,最大值在 PHP 5.5.6 之前是 100000,PHP
5.5.6 及之后是 1000000。也就是说在200到1000000之间。
opcache.max_accelerated_files=4000

;设置缓存的过期时间(单位是秒),为0的话每次都要检查
opcache.revalidate_freq=60

;从字面上理解就是“允许更快速关闭”。它的作用是在单个请求结束时提供一种更快速的机制来调用代码中的析构器,从而加快PHP的响应速度和PHP进程资源的回收速度,这样应用程序可以更快速地响应下一个请求。把它设置为1就可以使用这个机制了。
opcache.fast_shutdown=1

;如果启用(设置为1),OPcache会在opcache.revalidate_freq设置的秒数去检测文件的时间戳(timestamp)检查脚本是否更新。
如果这个选项被禁用(设置为0),opcache.revalidate_freq会被忽略,PHP文件永远不会被检查。这意味着如果你修改了你的代码,然后你把它更新到服务器上,再在浏览器上请求更新的代码对应的功能,你会看不到更新的效果
强烈建议你在生产环境中设置为0,更新代码后,再平滑重启PHP和web服务器。
opcache.validate_timestamps=0

;开启Opcache File Cache(实验性), 通过开启这个,
我们可以让Opcache把opcode缓存缓存到外部文件中, 对于一些脚本,
会有很明显的性能提升.
这样PHP就会在/tmp目录下Cache一些Opcode的二进制导出文件,
可以跨PHP生命周期存在.
opcache.file_cache=/tmp

更多关于PHP相关内容感兴趣的读者可查看本站专题:《PHP数学运算技巧总结》、《php操作office文档技巧总结(包括word,excel,access,ppt)》、《PHP数组操作技巧大全》、《php排序算法总结》、《PHP常用遍历算法与技巧总结》、《PHP数据结构与算法教程》、《php程序设计算法总结》、《php正则表达式用法总结》、《PHP运算与运算符用法总结》、《php字符串用法总结》及《php常见数据库操作技巧汇总》

查看phpinfo:

希望本文所述对大家PHP程序设计有所帮助。

奥门新浦京官方网站 22

测试结果:

奥门新浦京官方网站 23

同样的接口从以前的几百毫秒提升到现在的50ms左右

奥门新浦京官方网站 24

发表评论

电子邮件地址不会被公开。 必填项已用*标注