天天看点

不是所有的PHP项目都适合使用JIT提速#yyds干货盘点#

全称

JIT:Just In Time

好处

  1. 目前已经很难通过常规手段提升 PHP 的性能,JIT 基本上是目前性能提升的唯一手段;
  2. JIT 带来的性能提升可以让 PHP 在更多使用场景( CPU 密集)中发挥作用;
  3.  可以使用 PHP 来开发内置函数,而不用担心性能方面的问题。这一方面可以加速语言的发展(更多人可以参与进来),同时也可以减少目前使用 C 开发容易出现的内存管理、溢出等问题

简单来说:当JIT按预期工作时,你的代码将不会通过Zend VM执行,而是直接作为一组CPU级指令执行

PHP代码执行原理

每当要执行PHP代码(例如代码段或整个Web应用程序)时,都必须经过php解释器。最常用的是PHP FPM和CLI解释器,他们的工作很简单:接收php代码,对其进行解释并向后吐出结果。

执行流程:

  1. ​PHP代码被读取并转换为一组关键字,即标记(Tokens)。这个过程允许解释器理解在程序的哪一部分中写了哪段代码。这第一步叫做词法分析或符号化。
  2. 有了Tokens后,PHP解释器将分析这个Tokens集合,并尝试理解它们。结果通过一个称为解析的过程生成了一个抽象语法树(AST)。这个AST是一组指示应该执行哪些操作的节点。例如,“echo 1 + 1”实际上应该表示“打印1 + 1的结果”,或者更实际一些“打印一个操作,操作是1 + 1”。
  3. 有了AST,理解操作和优先级就容易得多了。将这个树转换成可以执行的东西需要一个中间表示(IR),在PHP中我们称之为操作码。将AST转换为操作码的过程称为编译。
  4. 现在,有了操作码,剩下就是执行代码。PHP有一个名为Zend VM的引擎,它能够接收操作码列表并执行它们。在执行了所有操作码之后,Zend VM就存在了,程序就终止了。​
不是所有的PHP项目都适合使用JIT提速#yyds干货盘点#

这里有一个瓶颈:如果php代码变化不是那么频繁,那么每次执行代码时对其进行词法分析又有什么意义呢?最后我们只关心操作码,所以这就是为什么存在Opcache扩展

Opcache扩展

Opcache扩展是随PHP附带的,通常没有什么理由禁用它。如果使用PHP,应该打开Opcache。

它的作用是为操作码在内存中添加一个共享缓存层。它的工作是从AST中新生成的操作码并缓存它们,以便进一步执行可以轻松跳过词法分析和解析阶段

不是所有的PHP项目都适合使用JIT提速#yyds干货盘点#

PHP使用Opcache的解释流程。如果文件已经被解析,则php会为其获取缓存的操作码,而不是再次解析。opcache完美地跳过了词法分析,语法解析和编译步骤。

注意:这就是PHP 7.4的预加载功能的亮点!它使你可以告诉PHP FPM解析代码库,将其转换为操作码并甚至在执行任何操作之前就对其进行缓存。

JIT即时编译器能做什么

如果Opcache可以更快地获取操作码,这样它们就可以直接转到Zend VM,那么JIT应该让它们在跳过Zend VM的情况下运行。

Zend VM是一个用C编写的程序,充当操作码和CPU本身之间的一个层。JIT所做的是在运行时生成编译后的代码,这样php就可以跳过Zend VM直接转到CPU。理论上讲,我们应该从中获得性能提升。起初,这对我来说很奇怪,因为为了编译机器代码,你需要为每种类型的体系结构编写一个非常具体的实现。但事实上,它是相当合理的。

PHP的JIT实现使用名为DynASM (Dynamic Assembler)的库,该库将一组特定格式的CPU指令映射为许多不同CPU类型的汇编代码。因此,JIT编译器使用DynASM将操作码转换为特定于架构的机器码。如果预加载能够在执行前将php代码解析为操作码,而DynASM可以将操作码编译为机器码(正好是及时编译),那么为什么我们不使用提前编译的方法直接编译php呢?PHP是弱类型的,这意味着PHP通常不知道变量的类型,直到Zend VM尝试执行某个操作码。这可以通过查看zend_value联合类型看出,它有许多指针指向一个变量的不同类型表示。无论何时Zend VM尝试从zend_value中获取值,它都会使用像ZSTR_VAL这样的宏来尝试从值联合中访问字符串指针。

例如,这个Zend VM处理程序应该处理一个“小于或等于”(<=)表达式。看看它是如何分支到许多不同的代码路径的,只是为了猜测操作数类型。用机器码复制这种类型推断逻辑是不可行的,可能会使事情变得更慢。在计算类型之后编译所有内容也不是一个好选择,因为编译成机器码是一项CPU密集型任务。所以在运行时编译所有东西也是不好的。

JIT即时编译器工作原理

我们不能在编译前推断出足够好的类型,而且在运行时进行编译是昂贵的。JIT对PHP有什么好处?为了平衡这个等式,PHP的JIT只尝试编译一些它认为可以得到回报的操作码。为此,它对Zend VM正在执行的操作码进行概要分析,并检查哪些操作码可以编译。(根据你的配置)

当编译某个操作码时,它将把执行委托给编译后的代码,而不是委托给Zend VM。看起来如下:

不是所有的PHP项目都适合使用JIT提速#yyds干货盘点#

如果已编译,则操作码不会通过Zend VM执行。所以在Opcache扩展中有一些指令检测某个操作码是否应该编译。如果是,编译器然后使用DynASM将该操作码转换为机器码,并执行新生成的机器码。有趣的是,由于当前实现中编译的代码有兆字节限制(也是可配置的),因此代码执行必须能够在JIT和解释代码之间无缝切换。

总结