TL; DR
PHP 8中的即时编译器作为Opcache扩展的一部分实现,旨在在运行时将操作代码编译为处理器指令。
这意味着使用JIT,某些操作代码不必由Zend VM解释,此类指令将直接作为处理器级指令执行。
PHP 8中的JIT
PHP 8中最受评论的功能之一是即时(JIT)编译器。在许多博客和社区中都可以听到它的声音-周围有很多嗡嗡声,但是到目前为止,我还没有找到有关JIT如何工作的许多细节。
经过多次尝试和无奈找到有用的信息后,我决定研究PHP源代码。结合我对C的一点了解以及到目前为止我已经收集的所有分散信息,我设法编写了这篇文章,希望它可以帮助您更好地理解PHP JIT。
为简单起见:当JIT按预期工作时,您的代码将不会通过Zend VM执行,而是直接作为一组处理器级指令执行。
这就是整个想法。
但是要更好地理解这一点,我们需要考虑php在内部如何工作。这不是很困难,但是确实需要一些介绍。
我已经写了一篇文章,概述了php的工作原理。如果您认为本文过于复杂,请阅读其前一篇然后再回来。这应该使事情变得容易一些。
PHP代码如何执行?
我们都知道php是一种解释语言。但是,这实际上意味着什么?
每当您要执行PHP代码时,无论是代码段还是整个Web应用程序,都必须经过php解释器。最常用的是PHP FPM和CLI解释器。他们的工作非常简单:获取php代码,对其进行解释,然后返回结果。
这是每种解释语言的共同画面。某些步骤可能有所不同,但总体思路是相同的。在PHP中,它的工作方式如下:
- 读取PHP代码并将其转换为一组称为Token的关键字。这个过程使解释器可以理解每段代码是写在程序的哪一部分中的。第一步称为Lexing或Tokenizing。
- , PHP . (Abstract Syntax Tree — AST) , (parsing). AST , , . , «echo 1 + 1» « 1 + 1» , , « , — 1 + 1».
- AST, , . -, , (Intermediate Representation IR), PHP (Opcode). AST .
- 现在我们有了操作码,最有趣的是:代码的实现!PHP具有称为Zend VM的引擎,该引擎能够获取操作码列表并执行它们。执行完所有操作码后,程序结束。
为了更清楚一点,我绘制了一个图:
PHP解释过程的简化图。
如您所见,非常简单。但是这里也有一个瓶颈:如果您的php代码可能不会经常更改,那么每次执行代码时对其进行词法分析和解析有什么意义?
毕竟,我们只对操作码感兴趣,对吗?对!这就是存在Opcache扩展的原因。
Opcache扩展
Opcache扩展是PHP附带的,通常没有特别的理由将其停用。如果您使用的是PHP,则可能应该启用Opcache。
它的作用是添加一个在线共享操作码缓存层。它的工作是从AST中获取最近生成的操作码并缓存它们,以便以后的执行可以轻松地跳过词法分析和语法分析阶段。
这是考虑到Opcache扩展的相同过程的示意图:
使用Opcache的PHP解释流程。如果文件已经被解析,则php会为其提取缓存的操作码,而不是重新解析它。
只是令人着迷的是,词典,解析和编译步骤被跳过的美妙程度。
注意:这是PHP 7.4预加载功能派上用场的地方!这使您可以告诉PHP FPM解析代码库,将其转换为操作码,甚至在实际执行任何操作之前就将其缓存。
您可能会开始想知道您可以在此处粘贴JIT,对吗?至少我希望如此,这就是为什么我写这篇文章的原因...
即时编译器有什么作用?
听了PHP Internals News播出的PHP和JIT播客中有关Ziva的解释之后,我对实际上应该做的JIT
有了一些了解... 如果Opcache允许更快的操作代码,以便可以直接转到Zend VM,JIT旨在使它完全不使用Zend VM。
Zend VM是一个C程序,充当操作代码和处理器本身之间的一层。JIT在运行时生成编译后的代码,因此php可以跳过Zend VM并直接跳转到处理器。从理论上讲,我们应该从性能方面受益。
起初听起来很奇怪,因为要编译机器代码,您必须为每种类型的体系结构编写一个非常具体的实现。但实际上这是很真实的。
PHP中的JIT实现使用DynASM(动态汇编程序)库,该库将特定格式的一组CPU指令映射到许多不同类型的CPU的汇编代码。因此,即时编译器使用DynASM将操作代码转换为特定于体系结构的机器代码。
尽管一个念头仍然困扰着我...
如果预加载能够在执行之前将php代码解析为可操作的代码,并且DynASM可以将可操作代码编译为机器代码(Just In Time编译),那么为什么我们不使用Ahead of Time编译立即就地编译PHP呢?
我从播客一集中得到的想法之一是,PHP是弱类型的,这意味着在Zend VM尝试执行特定的操作码之前,PHP常常不知道变量的类型。
您可以通过查看zend_value联合类型来理解这一点,该类型具有许多指向变量的不同类型表示形式的指针。每当Zend VM尝试从zend_value中获取值时,它都会使用ZSTR_VAL之类的宏正在尝试从值串联访问字符串指针。
例如,此Zend VM处理程序必须处理小于或等于(<=)表达式。了解它如何分支到许多不同的代码路径中,以猜测操作数的类型。
用机器代码复制这种类型的推理逻辑是不可行的,并且可能使事情变得更慢。
评估类型之后的最终编译也不是一个好选择,因为编译为机器代码是一项占用大量CPU的任务。因此,在运行时编译所有内容不是一个好主意。
即时编译器的行为如何?
现在我们知道,我们无法推断类型以生成足够好的预编译。我们也知道在运行时进行编译很昂贵。 JIT如何对PHP有用?
为了平衡这个方程,PHP JIT尝试仅编译它认为值得的一些操作码。为此,它将分析Zend虚拟机执行的操作码,并检查哪些编译有意义。 (取决于您的配置)。
编译特定的操作码后,它会将执行委派给该已编译的代码,而不是委派给Zend VM。如下图所示:
使用JIT的PHP解释流程。如果它们已被编译,则不会通过Zend VM执行操作码。
因此,Opcache扩展中有几条指令可以确定是否应编译某些操作代码。如果是这样,编译器将使用DynASM将其转换为机器代码,并执行此新生成的机器代码。
有趣的是,由于当前的实现对已编译代码(也可以配置)有一个兆字节限制,因此代码执行应该能够在JIT和解释代码之间无缝切换。
顺便说一下,Benoit Jacquemont的有关php的JIT的谈话对我非常有帮助。
我仍然不确定在什么情况下会进行编译,但是我想我真的还不想知道这一点。
因此您的生产率提高可能不会是巨大的
我希望现在更加清楚了,为什么每个人都在说大多数php应用程序都不会由于使用Just In Time编译器而获得很多性能优势。为什么Ziv推荐针对您的应用程序进行性能分析和不同JIT配置的建议是最佳的选择。
如果您使用的是PHP FPM,则编译后的操作码通常会散布在多个请求中,但这仍然不能改变游戏规则。
这是因为JIT优化了CPU的操作,如今,大多数php应用程序都以I / O为中心。无论如何,如果您必须访问磁盘或网络,则是否编译了处理操作都没有关系。时间将非常相似。
要是...
您正在执行非I / O的操作,例如图像处理或机器学习。除了I / O,其他任何东西都将从“即时”编译器中受益。这也是人们现在说他们更倾向于编写用PHP而不是C编写的本机PHP函数的原因。无论如何,如果编译这些函数,开销不会有太大的不同。
作为一名PHP程序员,有趣的时间...
希望本文对您有所帮助,并且您对PHP 8中的JIT有更好的了解。在这里,别忘了与您的其他开发人员分享此内容,它一定会为您的对话增加一点价值!
-- @nawarian
PHP: