当前位置: 首页 > 后端技术 > PHP

了解PHP8的JIT

时间:2023-03-29 14:17:09 PHP

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