PHP 性能分析与实验:性能的宏观分析

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

PHP 性能分析与实验:性能的宏观分析

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

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

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

 

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

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

PHP 性能分析与实验:性能的宏观分析

图1、PHP 语言解析运行过程

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

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

PHP 性能分析与实验:性能的宏观分析

图2. 启用了 opcode 缓存的 PHP 运行过程

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

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

PHP 性能分析与实验:性能的宏观分析

图3. 没有使用Opcode缓存的请求,20个并发,10000次

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

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

PHP 性能分析与实验:性能的宏观分析

图4、启用APC 缓存加速的实验结果

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

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

PHP 性能分析与实验:性能的宏观分析

图5、启用OpCode Cache 的性能分析结果

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

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

PHP 性能分析与实验:性能的宏观分析

图6、vld 扩展反编译出来的PHP代码的字节码

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

 

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

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

  1. <span class="pun"><?</span><span class="pln">php</span>
  2. <span class="pln">$a </span><span class="pun">=</span><span class="lit">10.11</span><span class="pun">;</span>
  3. <span class="pln">$b </span><span class="pun">=</span><span class="str">"30"</span><span class="pun">;</span>
  4. <span class="pln">var_dump</span><span class="pun">(</span><span class="pln">$a</span><span class="pun">+</span><span class="pln">$b</span><span class="pun">);</span>
  5. <span class="pln">var_dump</span><span class="pun">(</span><span class="str">"10"</span><span class="pun">+</span><span class="pln">$b</span><span class="pun">);</span>
  6. <span class="pln">var_dump</span><span class="pun">(</span><span class="lit">10</span><span class="pun">+</span><span class="str">"20"</span><span class="pun">);</span>
  7. <span class="pln">var_dump</span><span class="pun">(</span><span class="str">"10"</span><span class="pun">+</span><span class="str">"20"</span><span class="pun">);</span>

运行结果如下:

  1. <span class="kwd">float</span><span class="pun">(</span><span class="lit">40.11</span><span class="pun">)</span>
  2. <span class="kwd">int</span><span class="pun">(</span><span class="lit">40</span><span class="pun">)</span>
  3. <span class="kwd">int</span><span class="pun">(</span><span class="lit">30</span><span class="pun">)</span>
  4. <span class="kwd">int</span><span class="pun">(</span><span class="lit">30</span><span class="pun">)</span>

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

PHP 性能分析与实验:性能的宏观分析

图 7、要使用类型推断的 Swift代码

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

PHP 性能分析与实验:性能的宏观分析

图8、使用类型推断的 Swift 代码,编译速度很慢

但是如果调整代码如下:

PHP 性能分析与实验:性能的宏观分析

图9、避免了复杂数据类型推断的代码

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

PHP 性能分析与实验:性能的宏观分析

图10、减少了类型推断之后,编译速度大幅度提升

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

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

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

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

PHP 性能分析与实验:性能的宏观分析

图11、在PHP 5.3中,筛选法求素数的效率

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

PHP 性能分析与实验:性能的宏观分析

图12、在PHP 5.3中,改造成扩展后筛选法求素数的效率

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

  1. <span class="pln">PHP_FUNCTION</span><span class="pun">(</span><span class="pln">get_prime_numbers</span><span class="pun">)</span>
  2. <span class="pun">{</span>
  3. <span class="kwd">long</span><span class="pln"> value</span><span class="pun">;</span>
  4. <span class="kwd">if</span><span class="pun">(</span><span class="pln">zend_parse_parameters</span><span class="pun">(</span><span class="pln">ZEND_NUM_ARGS</span><span class="pun">()</span><span class="pln"> TSRMLS_CC</span><span class="pun">,</span><span class="str">"l"</span><span class="pun">,</span><span class="pun">&</span><span class="pln">value</span><span class="pun">)</span><span class="pun">==</span><span class="pln"> FAILURE</span><span class="pun">)</span><span class="pun">{</span>
  5. <span class="kwd">return</span><span class="pun">;</span>
  6. <span class="pun">}</span>
  7. <span class="kwd">int</span><span class="pun">*</span><span class="pln">numbers </span><span class="pun">=</span><span class="pun">(</span><span class="kwd">int</span><span class="pun">*)</span><span class="pln">malloc</span><span class="pun">(</span><span class="kwd">sizeof</span><span class="pun">(</span><span class="kwd">int</span><span class="pun">)*</span><span class="lit">128</span><span class="pun">*</span><span class="lit">10000</span><span class="pun">);</span>
  8. <span class="pln">memset</span><span class="pun">(</span><span class="pln">numbers</span><span class="pun">,</span><span class="lit">0x0</span><span class="pun">,</span><span class="lit">128</span><span class="pun">*</span><span class="lit">10000</span><span class="pun">);</span>
  9. <span class="kwd">int</span><span class="pln"> num </span><span class="pun">=</span><span class="lit">2</span><span class="pun">;</span>
  10. <span class="pln">numbers</span><span class="pun">[</span><span class="lit">0</span><span class="pun">]</span><span class="pun">=</span><span class="lit">2</span><span class="pun">;</span>
  11. <span class="pln">numbers</span><span class="pun">[</span><span class="lit">1</span><span class="pun">]</span><span class="pun">=</span><span class="lit">3</span><span class="pun">;</span>
  12. <span class="kwd">bool</span><span class="pln"> flag </span><span class="pun">=</span><span class="kwd">true</span><span class="pun">;</span>
  13. <span class="kwd">double</span><span class="pln"> f </span><span class="pun">=</span><span class="lit">0</span><span class="pun">;</span>
  14. <span class="kwd">int</span><span class="pln"> i </span><span class="pun">=</span><span class="lit">0</span><span class="pun">;</span>
  15. <span class="kwd">int</span><span class="pln"> j </span><span class="pun">=</span><span class="lit">0</span><span class="pun">;</span>
  16. <span class="kwd">for</span><span class="pun">(</span><span class="pln">i</span><span class="pun">=</span><span class="lit">5</span><span class="pun">;</span><span class="pln"> i</span><span class="pun"><=</span><span class="pln">value</span><span class="pun">;</span><span class="pln"> i</span><span class="pun">+=</span><span class="lit">2</span><span class="pun">)</span>
  17. <span class="pun">{</span>
  18. <span class="pln">flag </span><span class="pun">=</span><span class="kwd">true</span><span class="pun">;</span>
  19. <span class="pln">f </span><span class="pun">=</span><span class="pln"> sqrt</span><span class="pun">(</span><span class="pln">i</span><span class="pun">);</span>
  20. <span class="kwd">for</span><span class="pun">(</span><span class="pln">j</span><span class="pun">=</span><span class="lit">0</span><span class="pun">;</span><span class="pln"> j</span><span class="pun"><</span><span class="pln">num</span><span class="pun">;</span><span class="pln">j</span><span class="pun">++)</span>
  21. <span class="pun">{</span>
  22. <span class="kwd">if</span><span class="pun">(</span><span class="pln">i</span><span class="pun">%</span><span class="pln">numbers</span><span class="pun">[</span><span class="pln">j</span><span class="pun">]==</span><span class="lit">0</span><span class="pun">)</span>
  23. <span class="pun">{</span>
  24. <span class="pln">flag </span><span class="pun">=</span><span class="kwd">false</span><span class="pun">;</span>
  25. <span class="kwd">break</span><span class="pun">;</span>
  26. <span class="pun">}</span>
  27. <span class="kwd">if</span><span class="pun">(</span><span class="pln">numbers</span><span class="pun">[</span><span class="pln">j</span><span class="pun">]></span><span class="pln">f</span><span class="pun">)</span>
  28. <span class="pun">{</span>
  29. <span class="kwd">break</span><span class="pun">;</span>
  30. <span class="pun">}</span>
  31. <span class="pun">}</span>
  32. <span class="kwd">if</span><span class="pun">(</span><span class="pln">flag</span><span class="pun">)</span>
  33. <span class="pun">{</span>
  34. <span class="pln">numbers</span><span class="pun">[</span><span class="pln">num</span><span class="pun">]</span><span class="pun">=</span><span class="pln"> i</span><span class="pun">;</span>
  35. <span class="pln">num</span><span class="pun">++;</span>
  36. <span class="pun">}</span>
  37. <span class="pun">}</span>
  38. <span class="pln">free</span><span class="pun">(</span><span class="pln">numbers</span><span class="pun">);</span>
  39. <span class="pln">RETURN_LONG</span><span class="pun">(</span><span class="pln">num</span><span class="pun">);</span>
  40. <span class="pun">}</span>

 

三、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 秒左右。

PHP 性能分析与实验:性能的宏观分析

图13、在PHP 5.4中,筛选法求素数的效率

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

PHP 性能分析与实验:性能的宏观分析

图14、在PHP 5.5中,筛选法求素数的效率

PHP5.6 反而有些退步。

PHP 性能分析与实验:性能的宏观分析

图15、在PHP 5.6中,筛选法求素数的效率

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

PHP 性能分析与实验:性能的宏观分析

图16、在PHP 7中,筛选法求素数的效率

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

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

PHP 性能分析与实验:性能的宏观分析

PHP性能分析与实验(一)

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

PHP 性能分析与实验:性能的宏观分析

PHP性能分析与实验(一)

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

PHP 的详细介绍:请点这里
PHP 的下载地址:请点这里

相关推荐