PHP 8的即时编译器是Opcache扩展的一部分,旨在在运行时将某些操作码编译为CPU指令。
这意味着使用JIT,Zend VM不需要解释某些操作码,并且这些指令将直接作为CPU级指令执行。
PHP 8的JIT
PHP 8将带来的最受评论的功能之一是Just In Time(JIT)编译器。许多博客和社区都在谈论它,并且肯定会引起很大的轰动,但是到目前为止,我发现关于JIT应该做什么的细节很少。
经过多次研究和放弃后,我决定亲自检查PHP源代码。结合我对C语言的一点了解以及到目前为止所收集的所有分散信息,我提出了这篇文章,希望它也可以帮助您更好地理解PHP的JIT。
简化了事情:当JIT按预期工作时,您的代码将不会通过Zend VM执行,而是直接作为一组CPU级指令执行。
这就是整个想法。
但是要更好地理解它,我们需要考虑php在内部如何工作。不是很复杂,但是需要一些介绍。
我写了一篇博客文章,其中概述了php的工作原理。如果您认为此处的帖子太过密集,则只需检查另一个即可,稍后再回来。事情变得更容易理解。
PHP代码如何执行?
我们都知道php是一种解释语言。但这到底是什么意思?
每当您要执行PHP代码时(无论是代码段还是整个Web应用程序),都必须通过php解释器。最常用的是PHP FPM和CLI解释器。
他们的工作非常简单:接收php代码,对其进行解释,然后将结果返回回去。
通常,每种解释语言都会发生这种情况。有些人可能会删除一些步骤,但总体思路是相同的。在PHP中,它是这样的:
读取PHP代码并将其转换为一组称为Token的关键字。通过此过程,解释器可以了解在程序的哪个部分中编写了哪些代码。第一步称为Lexing或Tokenizing。
有了令牌,PHP解释器将分析此令牌集合并尝试使它们有意义。结果,通过称为解析的过程生成了抽象语法树(AST)。此AST是一组节点,指示应执行哪些操作。例如,“ echo 1 +1”实际上应表示“打印1 +1的结果”或更实际地是“打印操作,该操作为1 +1”。
例如,借助AST,可以更轻松地了解操作和优先级。将这棵树转换成可以执行的东西需要一个中间表示(IR),在PHP中我们称之为操作码。将AST转换为操作码的过程称为编译。
现在,有了Opcodes便是有趣的部分:执行代码!PHP具有称为Zend VM的引擎,该引擎能够接收操作码列表并执行它们。执行所有操作码后,Zend VM存在并且该程序终止。
我有一个图表,可以让您更加清楚:
有关PHP解释流程的简化概述。
如您所见,很简单。但是这里有一个瓶颈:如果您的php代码可能不会经常更改,那么每次执行代码时对其进行词法分析和解析有什么意义?
最后,我们只关心操作码,对吗?对!这就是存在Opcache扩展的原因。
Opcache扩展
Opcache扩展是PHP附带的,通常没有太大的理由要停用它。如果使用PHP,则可能应该打开Opcache。
它的作用是为操作码添加一个内存共享缓存层。它的工作是从AST中提取新生成的操作码并将其缓存,以便进一步执行可以轻松地跳过词法分析和语法分析阶段。
这是考虑了Opcache扩展的流程示意图:
PHP使用Opcache的解释流程。如果文件已经被解析,则php会为其获取缓存的操作码,而不是再次解析。
惊讶地看到它如何精美地跳过了Lexing,解析和编译步骤iling。
旁注:这就是PHP 7.4的预加载功能大放异彩的地方!它使您可以告诉PHP FPM解析代码库,将其转换为操作码并甚至在执行任何操作之前就将其缓存。
您可能想知道JIT的位置,对吗?我希望如此,这就是为什么我要写这篇文章的原因……
即时编译器有效地做什么?
在听完PHP Internals News的PHP和JIT播客专题节目中的Zeev的解释后,我对JIT的实际用途有了一些了解。
如果Opcache使获取操作码的速度更快,以便它们可以直接转到Zend VM,则应该使用JIT使它们完全在没有Zend VM的情况下运行。
Zend VM是用C编写的程序,充当操作码和CPU本身之间的一层。JIT的作用是在运行时生成编译的代码,因此php可以跳过Zend VM并直接进入CPU。从理论上讲,我们应该从中获得性能。
起初,这听起来很奇怪,因为要编译机器代码,您需要为每种类型的体系结构编写一个非常具体的实现。但实际上这是很合理的。
PHP的JIT实现使用名为DynASM(动态汇编程序)的库,该库将一种特定格式的一组CPU指令映射为许多不同CPU类型的汇编代码。因此,即时编译器使用DynASM将操作码转换为特定于体系结构的机器代码。
但是,有一个想法困扰了我很多时间了……
如果预加载能够在执行之前将php代码解析为操作码,并且DynASM可以将操作码编译为机器代码(及时编译),那为什么我们不立即使用Ahead of Time编译立即编译PHP?
通过听Zeev的一集,我得到的线索之一就是PHP的类型很弱,这意味着PHP通常在Zend VM尝试执行某个操作码之前才知道变量的类型。
通过查看zend_value联合类型,可以看出这一点,该类型具有许多指向变量的不同类型表示形式的指针。每当Zend VM尝试从zend_value中获取值时,它都会使用ZSTR_VAL之类的宏来尝试从值联合访问字符串指针。
例如,该Zend VM处理程序应处理“更小或等于”(<=)表达式。看一下它如何分支到许多不同的代码路径中,只是为了猜测操作数类型。
用机器代码复制这种类型推断逻辑是不可行的,并且可能使事情变得更慢。
在对类型进行求值后编译所有内容也不是一个好选择,因为编译为机器代码是一项占用大量CPU的任务。因此,在运行时编译所有内容也是不好的。
即时编译器的行为如何?
现在我们知道我们无法推断类型来生成足够好的提前编译。我们也知道在运行时进行编译很昂贵。JIT对PHP有何好处?
为了平衡此等式,PHP的JIT尝试仅编译一些认为可以产生回报的操作码。为此,它将分析Zend VM正在执行的操作码,并检查哪些代码可能有意义。(根据您的配置)
编译某个操作码后,它将把执行委派给该已编译代码,而不是委派给Zend VM。看起来如下:
PHP的JIT解释流程。如果已编译,则操作码不会通过Zend VM执行。
因此,在Opcache扩展中,有两条指令可检测是否应编译某个Opcode。如果是,则编译器然后使用DynASM将此操作码转换为机器代码,并执行此新生成的机器代码。
有趣的是,由于当前实现中已编译的代码以兆字节为单位(也是可配置的),因此代码执行必须能够在JIT和解释的代码之间无缝切换。
我仍不确定编译部分何时有效进行,但我想我现在暂时不想知道。
因此,您的性能提升可能不会很大
我希望现在更加清楚,为什么每个人都在说大多数php应用程序不会因为使用Just In Time编译器而获得巨大的性能优势。为什么Zeev建议为您的应用程序分析和试验不同的JIT配置是最好的方法。
如果使用PHP FPM,通常将在多个请求之间共享已编译的操作码,但这仍然不能改变游戏规则。
这是因为JIT优化了CPU约束的操作,并且当今大多数php应用程序都比任何东西受I / O约束更多。不管是否要访问磁盘或网络,处理操作是否已编译都没有关系。时间将非常相似。
除非…
您正在执行不受I / O约束的操作,例如图像处理或机器学习。任何不接触I / O的东西都将从“即时编译器”中受益。
这也是为什么人们现在说我们更愿意编写用PHP而不是C编写的本机PHP函数的原因。如果仍然编译此类函数,则开销将无法表达。
程序员的欢乐时光……