在故事的
第一部分中 ,基于Zend Technologies
的 Dmitry Stogov在HighLoad ++上的演讲,我们了解了PHP的内部结构。 我们首先详细了解了基本数据结构中的哪些变化使PHP 7加速了两倍以上。 这可能已经停止,但是在7.1版中,开发人员走得更远,因为他们仍然有许多优化想法。
现在可以解释在这七个方面之前在JIT方面积累的经验,查看不使用JIT的7.0中的结果以及使用JIT的HHVM的结果。 在PHP 7.1中,决定不使用JIT,而是再次转向解释器。 如果较早的优化涉及解释器,那么在本文中,我们将使用为我们的JIT实现的类型推断来研究字节码的优化。

在削减的范围内,Dmitry Stogov将使用一个简单的示例演示所有工作原理。
字节码优化
以下是标准PHP编译器将函数编译到的字节码。 它是单次通过-快速且愚蠢,但能够在每个HTTP请求上再次执行其工作(如果未连接OPcache)。

OPcache优化
随着OPcache的出现,我们开始对其进行优化。
OPcache早已内置了一些优化方法,例如,狭缝优化方法-当我们通过窥视孔查看代码,寻找熟悉的模式并将其替换为启发式方法时。 这些方法在7.0中继续使用。 例如,我们有两个操作:加法和赋值。

它们可以组合为一个复合赋值操作,该操作直接对结果执行加法运算:
ASSIGN_ADD $sum, $i
。 另一个示例是一个后递增变量,该变量理论上可以返回某种结果。

它可能不是标量值,必须将其删除。 为此,请使用其后的
FREE
指令。 但是,如果将其更改为预增量,则不需要
FREE
指令。

最后,有两个
RETURN
语句:第一个是RETURN语句在源文本中的直接反映,第二个是由带有右括号的傻瓜编译器添加的。 此代码将永远无法到达,可以删除。
循环中只剩下四个指令。 似乎没有什么可以优化的了,但对我们而言却不是。
查看
$i++
及其相应的指令
PRE_INC
预递增。 每次执行时:
- 需要检查什么类型的变量;
- 是否
is_long
; - 执行增量
- 检查溢出;
- 转到下一个
- 也许检查异常。
但是一个人,只要看一下PHP代码,就会看到变量
$i
处于0到100的范围内,并且不会有溢出,也不需要类型检查,也不会有异常。
在PHP 7.1中,我们试图教导编译器理解这一点 。
控制流程图的优化

为此,您需要推断类型,并输入类型,您必须首先构建计算机可以理解的数据流的形式表示。 但是,我们将从构建控件流图(控件依赖图)开始。 最初,我们将代码分为基本块-一组具有一个输入和一个输出的指令。 因此,我们在发生过渡的位置(即标签L0,L1)剪切代码。 我们还将其切入有条件和无条件分支运算符之后,然后将其与显示控制依赖关系的弧连接。

所以我们得到了CFG。
静态单作业表的优化
好了,现在我们需要一个数据依赖项。 为此,我们使用“静态单一分配表”-优化编译器世界中的一种流行表示形式。 这意味着每个变量的值只能分配一次。

对于每个变量,我们添加一个索引或轮回编号。 在每个进行分配的地方,我们都放置一个新索引,并在其中使用它们-直到问号为止,因为它并不总是在任何地方都知道。 例如,在指令
IS_SMALLER
$ i中,i既可以来自编号为4的块L0,也可以来自编号为2的第一个块。
为了解决这个问题,SSA引入
了Phi伪函数 ,必要时将其插入basic-> block的开头,获取从不同位置来到basic-block的一个变量的各种索引,并创建该变量的新轮回。 正是这些变量后来被用来消除歧义。

以这种方式替换所有问号,我们将构建SSA。
类型优化
现在我们推论类型-好像试图直接在管理上执行此代码。

在第一个块中,为变量分配了常量值-零,并且我们肯定知道这些变量的类型将是long。 接下来是Phi功能。 Long到达输入,我们不知道来自其他分支的其他变量的值。

我们相信输出phi()我们会很长。

我们进一步分发。 我们来介绍特定的功能,例如
ASSIGN_ADD
和
PRE_INC
。 加长两个。 如果发生溢出,则结果可能是long或double。

这些值再次落入Phi函数中,到达不同分支的可能类型集合的并集发生。 等等,我们继续传播,直到达到固定点,一切都安定下来。

我们在程序中的每个点都有一组可能的类型值。 这已经很好了。 计算机已经知道
$i
只能是long或double,并且可以排除一些不必要的检查。 但是我们知道
$i
不可能获得双倍的
$i
。 我们怎么知道 而且我们看到了一个条件,将
$i
在周期中的增长限制为可能的溢出。 我们将教计算机看到这一点。
范围传播优化
在
PRE_INC
指令中
PRE_INC
我们从未发现我只能是整数-它的成本是长整数或两倍。 发生这种情况是因为我们没有尝试推断可能的范围。 然后我们可以回答是否会发生溢出的问题。
范围的此输出以类似但略为复杂的方式进行。 结果,我们得到了具有索引2、4、6、7的固定范围的变量
$i
,现在我们可以自信地说,增量
$i
不会导致溢出。

通过合并这两个结果,我们可以肯定地说双精度变量
$i
永远不会成为。

我们得到的还不是优化,这是优化的信息! 考虑一下
ASSIGN_ADD
。 一般而言,该指令的总和的旧值可能是一个对象。 然后,添加后,旧值应已删除。 但是在我们的情况下,我们肯定知道有一个长或两倍,即一个标量值。 无需销毁,我们可以用
ADD
替换
ASSIGN_ADD
是一个更简单的说明。
ADD
将
sum
变量用作参数和值。

对于预递增操作,我们可以确定操作数始终很长,并且不会发生溢出。 我们为此指令使用了高度专业的处理程序,该处理程序将仅执行必要的操作而不会进行任何检查。

现在,在循环末尾比较变量。 我们知道变量的值只会很长-您可以通过将其与100进行比较来立即检查该值。 如果以前我们将验证结果记录在一个临时变量中,然后再次检查该临时变量的是/否,那么现在可以使用一条指令来完成,即简化。

字节码结果与原始结果相比。

周期中仅剩3条指令,其中两条非常专业。 结果,右边的代码
比原始代码
快3倍 。
高度专业的处理人员
任何
PHP爬网处理程序都只是一个C函数 。 左边是标准处理程序,右上角是高度专业化的处理程序。 左边的检查:操作数的类型,是否发生溢出,是否发生异常。 正确的仅添加一个,仅此而已。 它翻译成4条机器指令。 如果我们走得更远并进行JIT,则只需要一次
incl
指令。

接下来是什么?
我们将继续提高没有JIT的PHP分支7的速度。 在典型的综合测试中,
PHP 7.1会再次提高60% ,但是在实际应用中,这几乎无法取胜-在WordPress上仅为1-2%。 这不是特别有趣。 自2016年8月以来,冻结7.1分支以进行重大更改后,我们再次开始针对PHP 7.2或更确切地说PHP 8进行JIT研究。
在新的尝试中,我们使用
DynAsm生成代码,该代码由Mike Paul
为LuaJIT-2开发 。 这样做很好,因为它
可以非常快速地生成代码 :分钟数在LLVM上的JIT版本中编译的事实现在发生在0.1-0.2 s内。 今天,
JIT上bench.php的加速比PHP 5
快75倍 。
实际应用没有加速,这是我们面临的下一个挑战。 在某种程度上,我们获得了最佳代码,但是在编译了太多PHP脚本之后,我们阻塞了处理器缓存,因此它无法更快地工作。 而且代码速度并不是实际应用中的瓶颈...
也许DynAsm只能用于编译将由程序员或基于计数器的启发式方法选择的某些函数-函数被调用了多少次,其中重复了多少次循环,等等。
以下是我们的JIT为同一示例生成的机器代码。 最佳地编译了许多指令:递增为一条CPU指令,变量初始化为常数为两条。 在不填充类型的地方,您必须多花一点时间。

回到标题图片,与Mandelbrot测试中的类似语言相比,PHP显示了很好的结果(尽管数据在2016年底是相关的)。
该图以秒为单位显示执行时间,越少越好。也许
曼德布罗不是最好的测试。 它是计算性的,但是很简单,并且在所有语言中均等地实现。 很高兴知道Wordpress在C ++中的运行速度有多快,但是几乎没有什么奇怪的东西可以重写它以进行检查,甚至重复所有PHP代码的转换。 如果您有一套更合适的基准的想法-建议。
我们将于5月17日在PHP俄罗斯 举行会议 ,我们将讨论生态系统的前景和发展,以及将PHP用于真正复杂而酷炫的项目的经验。 已经与我们:
当然,这还不是全部。 征稿截止日期仍然截止, 直到4月1日, 我们正在等待那些能够应用现代方法和最佳实践来实现酷PHP服务的应用程序。 不要害怕与知名演讲者的竞争-我们正在寻找在实际项目中运用他们所做的工作的经验 ,并将帮助证明您的案件的益处。