澳门新浦京手机版你应该知道的 OpCode 缓存

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

什么是 OpCode 缓存

OpCode
缓存是PHP性能增强的扩展,它们通过将自己注入PHP的执行生命周期,并缓存编译阶段的结果,以便以后重用。近通过启用
OpCode 缓存即可看到3倍的性能提升并不罕见。

什么是opcode缓存?

当解释器完成对脚本代码的分析后,便将它们生成可以直接运行的中间代码,也称为操作码(Operate
Code,opcode)。Opcode
cache的目地是避免重复编译,减少CPU和内存开销。如果动态内容的性能瓶颈不在于CPU和内存,而在于I/O操作,比如数据库查询带来的磁盘I/O开销,那么opcode
cache的性能提升是非常有限的。但是既然opcode
cache能带来CPU和内存开销的降低,这总归是好事。

现代操作码缓存器(Optimizer+,APC2.0+,其他)使用共享内存进行存储,并且可以直接从中执行文件,而不用在执行前“反序列化”代码。这将带来显着的性能加速,通常降低了整体服务器的内存消耗,而且很少有缺点。

澳门新浦京手机版 1

什么时候该使用OpCode缓存

鉴于 OpCode
缓存几乎没有额外的内存使用(存储缓存)的副作用,它们应该始终在生产环境中使用。要担心的主要副作用是初始缓存造成的一些开销和缓存丢失(新服务器、重启
Apache / php-fpm、重启机器) 以及可能导致所谓的 cache
stampede 效应。当然,这可以通过启动高速缓存来缓解。

为什么要使用Opcode缓存?

这得从PHP代码的生命周期说起,请求PHP脚本时,会经过五个步骤,如下图所示:

澳门新浦京手机版 2

Zend引擎必须从文件系统读取文件、扫描其词典和表达式、解析文件、创建要执行的计算机代码(称为Opcode),最后执行Opcode。每一次请求PHP脚本都会执行一遍以上步骤,如果PHP源代码没有变化,那么Opcode也不会变化,显然没有必要每次都重行生成Opcode,结合在Web中无所不在的缓存机制,我们可以把Opcode缓存下来,以后直接访问缓存的Opcode岂不是更快,启用Opcode缓存之后的流程图如下所示:

澳门新浦京手机版 3

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

哪个 OpCode 缓存

在整个 PHP 生命周期中,都有一些 OpCode 缓存;第一个是来自
Zend,然后它是专有的。因此,在过去的几年中,主要是使用 APC 替代 PHP
缓存。虽然 APC 很优秀,但是它缺乏 Zend
的一些功能。另外还缺少维护人员,对其进行升级。

随着 PHP 5.5发布,Zend 开源了自己的缓存产品, 以 Zend OpCache
来命名。并将其包含在 PHP 中。

Zend OpCache 性能比 APC 更高,功能更加全面,更可靠。 然而,Zend OpCache
不包含由 APC 提供的辅助功能 –
用户变量缓存。为了缓解这个问题,发布了一个新的扩展 apcu,它只提供用户变量缓存功能,并且
100% 兼容 APC 的实现。

虽然 Zend 开源了自己的缓存方案,但是 APC 仍然广泛的使用者,并且对于
OpCode 缓存仍然是一个不错的选择。

有那些PHP opcode缓存插件?

Optimizer+(Optimizer+于2013年3月中旬改名为Opcache,PHP
5.5集成Opcache,其他的会不会消失?)、eAccelerator、xcache、APC …

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

安装

安装任何以下扩展后,你都必须要重启 PHP 才行,无论是通过重启 Apache 还是
PHP-FPM。

PHP opcode原理

Opcode是一种PHP脚本编译后的中间语言,就像Java的ByteCode,或者.NET的MSL,举个例子,比如你写下了如下的PHP代码:

 

  1. <?php
  2.    echo “Hello World”;
  3.    $a = 1 + 1;
  4.    echo $a;
  5. ?>

PHP执行这段代码会经过如下4个步骤(确切的来说,应该是PHP的语言引擎Zend)

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

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

那什么是Lexing?
学过编译原理的同学都应该对编译原理中的词法分析步骤有所了解,Lex就是一个词法分析的依据表。
Zend/zend_language_scanner.c会根据Zend/zend_language_scanner.l(Lex文件),来输入的
PHP代码进行词法分析,从而得到一个一个的“词”,PHP4.2开始提供了一个函数叫token_get_all,这个函数就可以讲一段PHP代码
Scanning成Tokens;

如果用这个函数处理我们开头提到的PHP代码,将会得到如下结果:

 

  1. Array
  2. (
  3.     [0] => Array
  4.         (
  5.            [0] => 367
  6.            [1] => Array
  7.         (
  8.             [0] => 316
  9.             [1] => echo
  10.         )
  11.     [2] => Array
  12.         (
  13.             [0] => 370
  14.             [1] =>
  15.         )
  16.     [3] => Array
  17.         (
  18.             [0] => 315
  19.             [1] => “Hello World”
  20.         )
  21.     [4] => ;
  22.     [5] => Array
  23.         (
  24.             [0] => 370
  25.             [1] =>
  26.         )
  27.     [6] => =
  28.     [7] => Array
  29.         (
  30.             [0] => 370
  31.             [1] =>
  32.         )
  33.     [8] => Array
  34.         (
  35.             [0] => 305
  36.             [1] => 1
  37.         )
  38.     [9] => Array
  39.         (
  40.             [0] => 370
  41.             [1] =>
  42.         )
  43.     [10] => +
  44.     [11] => Array
  45.         (
  46.             [0] => 370
  47.             [1] =>
  48.         )
  49.     [12] => Array
  50.         (
  51.             [0] => 305
  52.             [1] => 1
  53.         )
  54.     [13] => ;
  55.     [14] => Array
  56.         (
  57.             [0] => 370
  58.             [1] =>
  59.         )
  60.     [15] => Array
  61.         (
  62.             [0] => 316
  63.             [1] => echo
  64.         )
  65.     [16] => Array
  66.         (
  67.             [0] => 370
  68.             [1] =>
  69.         )
  70.     [17] => ;
  71. )

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

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

 

  1. echo a constant string
  2. add two numbers together
  3. store the result of the prior expression to a variable
  4. echo a variable

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

 

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

比如,我们的PHP代码会被Parsing成:

  1. * ZEND_ECHO ‘Hello World’
  2. * ZEND_ADD ~0 1 1
  3. * ZEND_ASSIGN !0 ~0
  4. * ZEND_ECHO !0

你可能会问了,我们的$a去那里了?

这个要介绍操作数了,每个操作数都是由以下俩个部分组成:

  1. op_type : 为IS_CONST, IS_TMP_VAR, IS_VAR, IS_UNUSED, or
    IS_CV b)
  2. u,一个联合体,根据op_type的不同,分别用不同的类型保存了这个操作数的值(const)或者左值(var)

而对于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了。

参考:

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

对于 PHP 5.5 及以上,Zend OpCache
默认都被编译为一个共享的扩展,除非你编译 PHP 时指定 –disable-all
。要启用它,你必须在 编译 PHP 时加上 –enable-opcache。

对于 PHP 5.4 或者更早的版本(> = 5.2),则可以使用 PECL 来安装:

$ pecl install zendopcache-beta

PECL 命令将尝试自动更新你的 php.ini 配置文件。你可以使用下面命令查看
pecl 将尝试更新的文件:

$ pecl config-get php_ini

它只是简单的将新的配置添加到指定文件顶部(如果有的话)。你也可以自己将它们移动到更合适的位置。

[zendopcache]
zend_extension=/full/path/to/opcache.so        ; 可能由 PECL 添加  
opcache.memory_consumption=128  
opcache.interned_strings_buffer=8  
opcache.max_accelerated_files=4000  
opcache.revalidate_freq=60  
opcache.fast_shutdown=1  
opcache.enable_cli=1  

如果你不知道扩展完整的路径,你可以看看 php.ini 中指定的 extension_dir
路径。另外,如果你通过 PECL
安装,它会输出一行(非常接近它的输出结尾),如下所示:/usr/local/php/lib/php/extensions/no-debug-non-zts-20100525/opcache.so

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

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

澳门新浦京手机版 4

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

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

澳门新浦京手机版 5

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

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

澳门新浦京手机版 6

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

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

澳门新浦京手机版 7

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

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

澳门新浦京手机版 8

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

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

澳门新浦京手机版 9

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

APCu – APC 的用户变量缓存(可选)

如果想使用 APC 的用户变量缓存,你还得安装 APCu。APCu 可以通过 PECL
安装。APCu 完全向后兼容 APC。APCu 不应该和 APC 一起安装。

$ pecl install apcu-beta

然后安装时,会提出两个问题,你可以直接使用这两个问题的默认值就好了。

和 Zend OpCache 一样,pecl
安装时可能已经为你在配置文件中增加了相应的行。当然,你可以将它们移到合适的位置。

[apcu]
extension=apcu.so                        ; 可能由 PECL 添加  
apc.serializer=php                        ; 有关详细信息,请参阅错误报告:http://ey.io/1aJhcOY  

配置好后,你现在可以使用apc_用户变量缓存功能了。

二、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 代码,我们尝试编译一下看看效果如何。 第一段代码如下:

澳门新浦京手机版 10

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

澳门新浦京手机版 11

但是如果调整代码如下:

澳门新浦京手机版 12

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

澳门新浦京手机版 13

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

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

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

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

澳门新浦京手机版 14

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

澳门新浦京手机版 15

可以想见,静态和编译类型的语言,其效率得到了惊人的提升。本程序的 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);
}
APC – 替代 PHP 缓存

要安装 APC,你将在此使用 PECL:

$ pecl install apc-beta

安装的时候,会有以下选项,你可以直接选默认值就好。

一旦安装成功,它将自动在 php.ini 中添加配置:

[APC]
extension=apc.so  
apc.enable=1  

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

澳门新浦京手机版 16

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

澳门新浦京手机版 17

PHP5.6 反而有些退步。

澳门新浦京手机版 18

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

澳门新浦京手机版 19

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

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

澳门新浦京手机版 20

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

澳门新浦京手机版 21

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

Engine Yard的PHP性能工具
  • 一套帮助改善 PHP 性能的工具
  • 开源项目,基于 Apache 2.0 许可
  • GitHub地址:
  • 可以使用 Composer 安装

要使用 Composer 安装,需要将以下内容添加到composer.json中:

"repositories":[
  {
    "type": "vcs",
    "url": "http://github.com/engineyard/ey-php-performance-tools"
  }
],
"require": {
  "php": ">=5.3.3",
  "engineyard/php-performance-tools": "dev-master"
}

然后在终端中执行下列命令:

$ composer update engineyard/php-performance-tools

这就将最新版本的 Engine Yard 的PHP性能工具安装好了。

使用

Zend OpCache 和 APC 在操作中都是透明的 – 当执行一个 PHP 文件时,OpCode
将被缓存,以备重用。

然后,正如我们所提到的,可能会引发 cache stampede效应。出现这种情况大多是系统负载过高,且缓存不可用的情况下

  • 因为尚未构建或由于某些原因,缓存已经被清除。

为了解决这个问题,你可以预先填充缓存,这被称呼为填充缓存

填充缓存(Zend OpCache)

不幸的是,还没有(当然,可能是我不知道)一个很好的方式来填充 Zend
OpCache。Zend OpCache
自己提供了一些方法来做到这些,不过我觉得不是很好用。

当然,你也可以使用一些笨办法来达到这一目的:

  1. 发送多个 Web 请求到服务器,从而导致 Zend OpCache 缓存生成的
    OpCode。 

    1. 只适合用于当前未处于负载状态的服务器
    2. 这可能是很多的请求,且需要一些时间来缓存所需的代码
  2. 在部署代码时,使用多个文档根目录来保存尽可能多的缓存。
HTTP 缓存

实现我们的第一选择的一种方法,就是使用我们的集成测试的测试套件(或其子套件)。如果没有的话,我们可以写个简单的爬虫脚本来完成这些请求。

在Engine
Yard的PHP性能工具库中,提供了一个简单的
HTTP Cache Primer 工具。这个简单的脚本使用 pecl_http
扩展并行地执行多个请求,在 MacBook Air 上,大约 4 秒执行超过 100
个请求。

如果没有安装 pecl_http,脚本会一个个顺序执行,会比并行执行慢很多

要使用它,请在当前目录中创建一个基于 config.php-dist的config.php,其中包含你希望执行的
URL 列表,如下:

return [  
  /* URLs to cache */
  'urls' => [
    'http://xxx.com/',
    'http://xxx.com/user/login',
    'http://xxx.com/user/register',
    'http://xxx.com/privary'
  ],
  /* 需要多少并发, 需要 pecl_http 扩展的支持 */
  'threads' => 3
]

然后我们运行它:

$ /path/to/vendor/bin/cache-primer

这将产生如下输出:

=== Cache Primer ===
Attempting to cache 4 URLs:  
Running in parallel with 3 concurrent requests  
...!
Cached 3 URLs in 0.6162059307098 seconds  
Encountered 1 errors  

或者使用更多的 URL:

=== Cache Primer ===
Attempting to cache 104 URLs:  
Running in parallel with 10 concurrent requests  
............................................................................!...........................
Cached 103 URLs in 4.6162059307098 seconds  
Encountered 1 errors  

以这种填充方式的一个优点是,几乎可以填充任何 HTTP 高速缓存,例如
Varnish 或 Squid

Zend OpCache 自带的方式

新的 Zend OpCache 中
添加了 opcode_compile_file(),只需要使用 zend-primer 就可以填充当前启用的缓存。

填充缓存 (APC)

你可以想 Zend OpCache 一样,使用 HTTP 请求来填充缓存,你也可以直接使用
APC 提供的直接填充缓存的方法。而我们仍然需要向 Web
服务器发送请求,我们可以用单个请求缓存整改代码库。

我们可以使用 APC
提供的 apc_compile_file()函数做到这一点,该函数将编译指定文件,并将其添加到缓存中。这意味着,我们可以编译任何有效的
PHP 文件,类,模版等。

类似于 HTTP
填充缓存,我们需要创建一个基于 config.php-dist 的 config.php 文件,只是这次它包含的不再是
URL 列表,而是编译文件的目录列表,以及 HTTP Basic 身份 (可选) 配置。

HTTP Basic
身份验证为此脚本提供了最少的保护,更好的方式是禁用此身份验证,并将脚本包含在系统管理界面中。

if (!defined('DS')) {  
  define('DS', DIRECTORY_SEPARATOR);
}

return [  
  /* 身份验证 */
  'auth' => [
    'enable' => true,
    'username' => 'admin',
    'password' => 'admin',
  ],

  /* 缓存路径(递归) */
  'paths' => [
    __DIR__ . DS . '..' . DS . '..' . DS . 'config',
    __DIR__ . DS . '..' . DS . '..' . DS . 'module',
    __DIR__ . DS . '..' . DS . '..' . DS . 'vendor',
    __DIR__ . DS . '..' . DS . '..' . DS . 'public'
  ]
];

创建此文件后,你可以将其打包成 phar
归档包,或者将其集成到任何其他已有的管理界面中。

当你请求此文件时,先进行身份验证,验证成功后,会递归遍历配置中的内容,查找具有
.php 和 .phtml 扩展名的所有文件;
然后过滤掉重复文件,最后将它们添加到缓存中。

当运行时,会得到类似下面的输出:

Cached 4841 files in 2.1302671432495 seconds  

这种方式只需要一次请求即可完成,为实际客户端留下更多的资源。

但是,缺点在于,我们不能像之前的方式那样可以填充 Varnish 或者其他缓存。

底层

无论你使用哪种
OpCache,它们的工作方式几乎是一样的。它们将自己注入到执行生命周期中,并在对时间内重复使用。Zend
OpCache 还将在创建缓存时执行一些优化。

执行生命周期

PHP
时一种脚本语言,大多数人认为它不是编译执行的。虽然传统意义上,这确实是正确的,因为我们没有调用
GCC 或者 javac;事实上,每次请求 PHP 脚本时,都经过了编译操作。其实 PHP
和 JAVA 的生命周期时非常相似的,因为它们都编译为中间指令集(OpCode
或者字节码),然后在虚拟机 (Zend VM 或 JVM) 中运行。

澳门新浦京手机版 22

解析和编译阶段很慢。当我们添加一个 opcache
时,我们通过存储解析和编译极端的结果来缩短这个过程,只需要执行一次就可以动态的运行。事实上,我们现在更接近于
JAVA
的生命周期;主要区别是我们保存到共享内存中,而不是文件中,并且如果脚本发生变更,可以自动重新编译。

澳门新浦京手机版 23

Token 和 OpCodes

一旦获得了你的 PHP 代码,就会创建两个标识码。第一个是
Token,这是一种将代码分解为小块的方法,以便引擎将其编译成第二种码:OpCodes。OpCodes
是Zend VM 执行的实际指令。

用 Hello World 来看 Token 和 OpCode

你学编程写的第一个程序是不是 Hello World? 应该是吧! 那我们这里也用
Hello World 来演示下 Token 和 Opcodes。

<?php  
class Greeting {  
  public function sayHello($to) {
    echo "Hello $to";
  }
}

$greeter = new Greeting();
$greeter->sayHello("World");
?>
令牌化
Token 名
T_OPEN_TAG <?php
T_CLASS class
T_WHITESPACE  
T_STRING Greeting
T_WHITESPACE  
  {
T_WHITESPACE  
T_PUBLIC public
T_WHITESPACE  
T_FUNCTION function
T_WHITESPACE  
T_STRING sayHello
  (
T_VARIABLE $to
  )
T_WHITESPACE  
  {
T_WHITESPACE  
T_ECHO echo
T_WHITESPACE  
  "
T_ENCAPSED_AND_WHITESPACE Hello
T_VARIABLE $to
  "
  ;
T_WHITESPACE  
  }
T_WHITESPACE  
  }
T_WHITESPACE  
T_VARIABLE $greeter
T_WHITESPACE  
  =
T_WHITESPACE  
T_NEW new
T_WHITESPACE  
T_STRING Greeting
  (
  )
  ;
T_WHITESPACE  
T_VARIABLE $greeter
T_OBJECT_OPERATOR ->
T_STRING sayHello
  (
T_CONSTANT_ENCAPSED_STRING "World"
  )
  ;
T_WHITESPACE  
T_CLOSE_TAG ?>

如上所示,大部分代码片段的名字都是以 T_ 开头然后是一个描述性的词。这是臭名昭着的TPAAMAYIMNEKUDOTAYIM错误来自:它代表双冒号。

有些令牌没有T_名称,这是因为它们是一个单一的字符,如果这里给它们分配一个更大的名字,显然没有意义而去很浪费。

上面看到变量的插值也很有意思(“Hello
$to”),采用两个字符串。第一个,将插值变量分为四个独特的操作码:

  "
T_ENCAPSED_AND_WHITESPACE Hello
T_VARIABLE $to
  "

然后非插值字符串只是一个:

T_CONSTANT_ENCAPSED_STRING "Worl"

有了这个观点,我们可以看到我们如何以很小的方式开始影响性能。

用于生成 Token 名单的脚本:

#!/usr/bin/env php
<?php  
if (!isset($_SERVER['argv'][1])) {  
    echo "Usage: {$_SERVER['argv'][0]} <filename>" . PHP_EOL;
    exit;
}
$tokens = token_get_all(file_get_contents($_SERVER['argv'][1]));
foreach ($tokens as $token) {  
    if (is_integer($token[0])) {
        echo str_pad(token_name($token[0]), 28);
        echo trim($token[1]);
    } else {
        echo str_repeat(" ", 28);
        echo $token[0];
    }
    echo PHP_EOL;
}
?>
OpCodes

接下来,让我们看看编译成 OpCodes 后的样子。

要获取 OpCodes,我们需要安装 VLD (Vulcan Logic Dumper) 扩展。

$ pecl install vld-beta

并确保以下内容已经成功添加到 php.ini 中:

extension=vld.so  

安装成功后,你可以在命令行中,使用以下命令 dump 任何代码的 OpCodes:

$ php -dvld.active=1 -dvld.execute=0 <file>

VLD dumps
全局代码(主脚本),全局函数和类函数。但是,我们先看看我们的类函数,以便遵循与代码本身相同的流程。

了解 VLD dumps

VLD dumps 通常是多个
dumps,一个用户主脚本,一个用户每个全局函数和类函数,每个 dump
结构相同。

首先列出(如果适用)该类和函数,然后列出文件名,接下来是函数的名称。

Class Greeting:  
Function sayhello:  
filename:      ./Greeting.php  
function name:  sayHello  

接下来,它列出了 dumps 中的 OpCodes 总数:

number of ops:  8  

再然后,列出编译的变量:

compiled vars:  !0 = $to  

最后,它列出了 OpCodes ,每一行,标题行下:

line      # *  op                     fetch        ext  return  operands  
------------------------------------------------------------------------

每个 OpCodes 有以下几点:

  • line: 在源文件中的行号
  • *:进入点(左对齐)和出口点(右对齐)
  • op:OpCodes 名
  • fetch:全局变量详细信息
  • ext:与 OpCodes 相关联的额外数据
  • return:返回数据的位置
  • operands:OpCodes 使用的操作数(例如两个变量的连接)

并非所有列都适用于所有 OpCodes。

变量

Zend Engine 中有多种类型的变量。所有变量都使用标识符。

  • 带有感叹号(!) 前缀的变量是编译变量(CVs) – 这些是用户空间变量的指针
  • 前缀为波浪号(~)的变量是用于进程内操作的临时存储(TMP_VAR)的临时变量
  • 以$($)为前缀的变量是另一种类型的临时变量(VAR),它们与用户空间变量(如CVs)相关联,因此需要重新计数。
  • 以冒号(:)作为前缀的变量是用于将查询结果存储在类哈希表中的临时变量
Dumping Hello World
Class Greeting:  
Function sayhello:  
number of ops:  8  
compiled vars:  !0 = $to

line      # *    op                      fetch          ext     return     operands  
----------------------------------------------------------------------------
   3      0  >   EXT_NOP
          1      RECV                                         !0
   5      2      EXT_STMT
          3      ADD_STRING                                   ~0    'Hello+'
          4      ADD_VAR                                      ~0    ~0, !0
          5      ECHO                                                 ~0
   6      6      EXT_STMT
          7    > RETURN                                               null

我们看到我们在类Greeting中的函数sayHello中,我们有一个编译变量$
to,它由!0标识。

接下来,按照操作码列表(忽略no-op),我们可以看到:

  • RECV:该函数接收分配给!0(表示$to)的值
  • ADD_STRING:接下来,我们创建一个由~,~0标识的临时变量,并分配静态字符串’Hello
    +’,其中+表示空格
  • ADD_VAR:之后,我们将我们的变量!0的内容连接到我们的临时变量~0
  • ECHO:然后我们打印这个临时变量
  • RETURN:最后,功能结束时我们什么也不返回

接下来,看看主脚本:

这里我们看到一个编译变量$greeter,由!0标识。所以让我们来看看这一点,我们再次no-op的操作。

  • FETCH_CLASS:首先我们查找类Greeter; 我们将此参考存储在 :1
  • NEW:然后我们实例化一个类的实例(:1),并将其分配给一个VAR,$2
  • DO_FCALL_BY_NAME:调用构造函数
  • ASSIGN:接下来我们将生成的对象(VAR $2)分配给我们的CV(!0)
  • INIT_METHOD_CALL:我们开始调用sayHello方法
  • SEND_VAL:将"World"传递给该方法
  • DO_FCALL_BY_NAME:执行该方法
  • RETURN:脚本成功结束,隐式返回值为1

去对你的应用进行优化吧

使用 OpCode,它会给你的应用提升的性能,另外使用 Zend OpCache
你可以对你的项目进行大量的优化。

使用操作码缓存不应该是可选的,它将使你能够以很少的精力从硬件中获得更多的性能。如果你还没有使用,为什么不呢?

资源

  • Zend OpCache 源码: 
  • Zend OpCache PECL 包: 
  • APC 源码: 
  • APC PECL 包: 
  • 了解 Opcodes (Sara
    Golemon): 
  • 使用 VLD 进行多源分析 (Derick
    Rethans): 

写到凌晨 1:42 才写完,而且还是在公司写的。😂 这就是我的五一假期。

发表评论

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