深入研究PHP8的JIT

2020-08-30 08:04:36

PHP从其最新的主要版本PHP8开始就有了即时编译器(JIT)。

下面是JIT对PHP影响的演示。这段视频是由php引擎的核心开发者zeev录制的,目的是演示php7.0和jit在生成分形时的性能差异。

看完这段视频后,预期性能会有很大的提高是合理的,特别是如果你还记得从php 5迁移到php 7时的惊人提升,但很快你就会注意到,对于大多数应用程序来说,这样的提升是不可能的(除非你为活着的🤷🏻‍♀️做分形)。

因此,在您对JIT过于炒作或过于失望之前,让我来解释一下它是什么,以及如何从它中获得最大收益。

CPU不会读英语,这一点你已经知道了。它们可以某种程度上读取代表地址、值、指令的一系列位和字节...。但是猜猜谁不能给这个编码呢?没错,我们!)至少我可以(👀)(我差不多可以,但是……。不是))。

这个问题的一个解决方案是将我们人类可以写的东西翻译成CPU可以执行的东西。这个翻译可以称为编译。所以编译就是人类可读代码到机器代码的翻译。

将人类可读代码转换为机器代码可以通过三种不同的方式进行:提前(AOT)编译、即时(JIT)编译,或者我们喜欢的解释(或隐式编译)。

像C、C++或Rust这样的语言都是AOT编译的语言。您编写代码,编译它,输出是一个CPU立即理解为要执行的二进制对象。之所以提前调用它,是因为您编译了运行它的二进制objectBEFORE。

PHP、Java或JavaScript等语言是解释语言。用这种语言编写的代码将被翻译成中间表示,而另一个程序(编译后的程序)将理解并执行这种中间表示。将这样的程序称为";虚拟机";或";解释器";是非常常见的。

与AOT编译的语言相比,正常解释的语言的性能和自由度较低,因为用户空间不提供内存管理、以CPU为目标的指令和时髦的低级技巧。因此,另一个程序(解释器)必须管理所有这些,再加上要与不同类型的处理器和操作系统对话。

像Java、Lua和现在的PHP这样的语言不仅是解释的,而且是Justin Time编译的!他们仍然需要VM,但是他们的VM能够在运行时将代码的这种中间表示形式编译成二进制对象。支持JIT的VM以混合模式运行,其中代码被部分编译和解释。

当JIT生效时,你的部分php代码会被编译成机器码,这样Zend VM(php#39;的虚拟机)就不会再解释这些部分了。您的phpcode将直接与CPU对话。

那么,为什么我们不能把所有的东西都汇编起来呢?如果VM是一个瓶颈,那么为什么要在一开始就麻烦地运行它呢?我们可以将php代码编译成机器码,并为此感到高兴。

嗯,我们必须编译到特定的CPU体系结构,准备好与不同的操作系统对话,非常清楚我们的变量类型,并执行比我们想要更高效的裸机操作更多的裸机操作。例如,Golang和Rust有不错的抽象,但它们非常实用,需要控制它们部署到的环境。

另一方面,解释型语言可以很容易地部署到不同的服务器上,允许快速开发,并为动态键入系统创造空间。

即时编译可能是这两个领域中最好的,它结合了良好的速度和开发人员的体验。

如您所知,动态类型是PHP的核心特性之一。它非常复杂,并且以一种几乎不可想象的方式纹身到了php的核心中,移除动态类型几乎是不可想象的。下面是关于php的类型系统如何工作的很好的总结。

而且由于动态类型(以及您将在下面了解的其他一些内容),PHP中的即时编译器并没有带来很大的性能提升,至少现在还没有。

在PHP中,在执行代码之前有三个步骤:标记化、解析成AST和编译成称为操作码的中间表示形式。

标记化(或词法分析)是读取php代码并将其拆分成称为令牌的可理解单元的过程。<;?PHP变成_T_OPEN标记,ECHO变成_T ECHO,";Hello,Friend";变成T_CONSTANT_ENCAPSED_STRING。PHP令牌的完整列表可以在这里找到。

解析是理解这些标记的过程。在PHP中,解析后的标记被组织在一个名为AST(抽象语法树)的树形结构中,AST的工作是表示应该执行哪些操作。在回应1+1中,解释器实际上应该理解打印表达式1+1的结果。这样的树应该类似于以下内容:

操作=>;回显,操作数=>;表达式(操作=>;加法,操作数1=>;1,操作数2=>;1)。

操作码是虚拟机实际执行的代码,所以执行是最后一步。这里有一张图表,说明了这个过程是什么样子。

您可能意识到,每次对代码进行标记化、解析和编译都可能是一个很大的瓶颈。PHP工程师也是这么想的,这就是Opcache扩展存在的原因。让我们快速地看一看。

Opcache扩展是随PHP一起提供的,通常没有什么重要的理由将其停用。如果您使用PHP,您可能应该打开Opcache。

它添加了内存中的共享缓存层来存储操作码。因此,标记化、解析和编译将为每个文件执行一次,并将与每个请求共享。

附注:这就是PHP7.4';的预加载特性提升的地方!它允许您告诉PHP FPM解析您的代码库,将其转换为操作码,并在执行任何操作之前缓存它们。

虽然Opcache扩展将防止PHP反复标记、解析和编译,但即时编译的目的是跳过虚拟机的sOpcode解释,让它直接执行机器码。

PHP的JIT实现使用一个名为DynASM(动态汇编程序)的C库,它将一组特定格式的CPU指令映射为任何不同CPU类型的汇编代码。因此,即时编译器使用DynASM将操作码转换为特定于体系结构的机器码。

编译是在从缓存获取操作码和执行操作码之间进行的,因为将操作码编译成机器码可能非常昂贵,所以PHP必须决定编译代码的哪些部分可能有意义。

然后,PHP分析Zend VM正在执行的操作码,并检查哪些操作码可能需要编译。(根据您的配置)。

当编译操作码时,它的执行不是通过Zend VM处理程序进行的,而是由CPU直接执行。

何时编译操作码取决于您的INI配置。你将在下一节得到更多细节。

在PHP中配置JIT非常简单,我们需要设置两个INI指令:opcache.jit_buffer_size和opcache.jit。第一个指示我们愿意为编译的代码分配多少内存,而后一个指示JIT应该如何行为。

还有一个名为opcache.jit_debug的可选指令,用于调试目的。

例如,下面的代码片段表明,我们愿意放弃高达100兆的编译代码,从而使用全局线性扫描寄存器分配器生成AVX指令,分析每个请求并跳转热函数,基于静态类型推断优化编译代码。(👀wutt?)。

为了让你的生活更轻松,我带来了一些预设,本杰明·埃伯雷(Benjamin Eberlei)在他的博客中亲切地分享了这些预设:

Opcache.jit条目是名为CRTO&34;的值序列,每个字符可以对应用程序产生不同的行为影响。下面我解释一下每封信都能做些什么:

我并不完全理解其中的每一个术语,但主要的结论是,您应该尝试一下每个旗帜,并不断分析您的应用程序,以便更好地理解哪些更适合您。

性能结果可能非常违反直觉。例如:更高的JIT缓冲区大小可能会导致应用程序变慢,因为PHP可能会花更多的时间编译更多的操作码,而不是执行它们。(缓冲区越大,可以编译的越多)

Just in Time编译器将尽最大努力将操作码转换为机器码,但当然可能会出现一些问题。

PHP的类型系统非常宽容,它的灵活性可能会给Zend VM带来很大的开销。将其类型转换为机器码可能最终生成的编译代码在运行时比解释它的成本更高,而据我所知,STRICT_TYPE模式根本帮不上忙。

[看看这个VM处理程序](这个Zend VM处理程序),可以清楚地看到php的类型调整在运行时可以分支到如此多的可能性,将它们组合在一起最终可能比仅仅通过VM解释要花费更多的成本。

因为JIT绕过了一些VM钩子,所以像xDebug这样的工具在跟踪jited代码时会遇到一些麻烦,这是意料之中的。

有人可能会争辩说,JIT应该是一个仅限生产的特性。但它也可能通过意外更改代码行为而导致错误,因此没有一个简单的调试解决方案可能会带来麻烦。

对于终端用户来说,这并不是什么大问题,但是JIT大大增加了PHP记分库的复杂性,而且它未来的范围还包括更疯狂的实现细节。

根据您的设置和用例不同,PHP的性能可能会翻一番。在其早期实施阶段,出现了一些非常令人印象深刻的数字:

它们都有什么共同之处呢?它们是CPU密集型(在一定程度上)任务,这些任务将从PHP中的JIT中获益最多。

目前还不太清楚现实世界中的应用程序在JIT中将如何运行,但这在很大程度上取决于您的配置和用例。由于php应用程序通常会执行许多I/O操作,它们可能不会感觉到太多改进,但可以将JIT引擎定位到应用程序的某些位置。

几乎任何在代码中经常重复且完全没有I/O依赖性的任务。

如果我猜一下我们已经使用的哪种工具可以立即从JIT中获益。

如果我猜一下,我们已经使用的哪些工具可以立即从JIT中获益,它们是Composer、Psalm、PHP-Parser、PHPCS和所有的异步PHP框架。

许多人以前说过,PHP在性能改进方面碰壁了,这意味着从现在开始,每一次改进都将花费更多的工作,而产生的收益更少。

JIT为php语言带来了新的视野。Php代码可以像asc一样快的想法将允许我们的php应用程序超越web环境。

像PHP游戏开发这样疯狂的应用程序可能会变得更加普遍,至少是为了好玩。

我相信在PHP8发布之后,Swoole应用程序会慢慢变得更加流行。它们的长期运行和异步特性可以从Jitin中获益,这也许是快速的基于代理的模型无法做到的。

与JIT本身相关的是,已经有一个想法是添加一个低成本的初始概要,它将收集足够的数据来计算跳转的概率,并获取运行时类型和值。这将允许引擎只对快速路径进行JIT,并优化VM-Machine代码交换。

我希望本文对您有帮助,并希望您能够更好地理解PHP8';sJIT是关于什么的。

如果您想在这里添加一些我可能忘记的东西,请随时在Twitter上联系我,并且不要忘记与您的开发人员同事分享这些内容,它肯定会为您的对话增加很多价值!