深入了解PHP8 JIT(即时编译)功能
(Just-In-Time)即时编译器是PHP 8.0中最重要的新功能之一。JIT可以通过将PHP应用程序的全部或经常调用的部分作为CPU机器代码编译并存储并直接执行,从而绕过Zend VM及其过程开销,从而提高性能。
JIT是传统解释器和AOT编译器的混合体。混合模型带来了这两种方法的利弊,而经过微调的应用程序可以胜过JIT的弊端。
PHP的JIT实施是Dmitry Stogov付出的巨大努力,历时数年之久的讨论,实施和测试都是如此。
PHP JIT:
PHP 8.0的JIT基础概述和配置选项,请参阅PHP 8.0:JIT。
这篇文章是关于基准,JIT如何工作以及理想的配置选项的。
大多数PHP应用程序都接受HTTP请求,从数据库中检索和处理数据,并返回结果。IO通常是重要的性能瓶颈:从磁盘读取数据,写入和网络请求。
PHP 8.0引入了JIT,以提高PHP应用程序的性能,但它也增加了调试的障碍,因为应用程序的某些部分可能作为CPU机器代码缓存,而标准PHP调试器无法使用。PHP 8.0的JIT pull-request在PHP代码库中增加了50,000多个新行,因此,除了从事JIT的开发人员之外,PHP核心开发人员本身可能并不精通。
PHP虚拟机
PHP代码一旦处理(标记,解析,构建AST和构建操作码),便在Zend虚拟机上运行。与Java和JavaScript相似,虚拟机对应用程序的硬件方面进行了抽象,从而可以“运行” PHP源代码而无需编译。
Opcache扩展可以帮助将操作码存储在共享内存中,从而跳过重复的标记化/解析/操作码步骤。
PHP已经包含了一些优化措施,例如在Opcode级别上消除了死代码,但是不可能在虚拟机级别之外执行优化,因为在这一点上,代码是由虚拟机解释的,而不是编译代码。
移交给其他应用程序
PHP已经具有一些集成,可以调用已编译的其他应用程序。
GD扩展可能是最接近的。如果PHP要在矢量或位图级别处理图像,那将是非常慢的,这是因为PHP附加了虚拟机层。GD扩展调用已编译的二进制文件,可以利用高级CPU指令执行相同的操作。
PHP 7.4引入了Foreign Functions Interface(也是Dmitry的工作),它提供了一个统一的接口来调用任意应用程序而无需开发PHP扩展。借助FFI,可以将C和Rust等传统编译语言与PHP集成在一起。
编译PHP代码
尽可能接近CPU的自然下一步就是跳过虚拟机,这就是JIT。
即时编译是JavaScript早在多年前就被V8引擎成功采用的一项功能。其他语言也以一种方式或另一种方式实现JIT。最大的优点是仍然不需要预编译源代码,但是有了共享的已编译机器代码的缓存,该语言可以触发代码与已编译机器代码一起执行,稍后再编译或不使用JIT来执行。
LLVM
LLVM是一种流行的编译工具集,帮助开发编译器对于今天大多数AOT语言。
LLVM的目标包括x86,x86-64和其他几种类型,包括图形处理器,Web汇编器,ARM等。
PHP考虑使用LLVM,但由于编译器速度不理想,因此效果不佳。
DynASM
LuaJIT项目的DynASM对于PHP的JIT来说要快得多。与LLVM相比,它对目标CPU指令集的支持是有限的,但是它对x86和x86-64指令集提供了支持。服务器端编程语言(例如PHP)最常用的语言。
PHP 8.0的JIT实现使用DynASM进行代码生成。PHP的JIT受DynASM对目标处理器体系结构的限制的约束。
PHP JIT如何工作
PHP JIT被实现为Opcache的一部分。这使JIT与PHP引擎分离。
JIT的三个组件是使用虚拟机或直接使用存储在缓冲区中的机器代码来存储,检查和无缝调用代码。
缓冲
JIT缓冲区是存储已编译的CPU机器代码的位置。PHP提供了配置选项(opcache.jit_buffer_sizeINI设置)来控制应为JIT缓冲区分配多少内存。
触发器
Opcache中的触发器负责在遇到代码结构时调用已编译的机器代码。这些触发器可以是函数调用项,循环等。
跟踪器
JIT跟踪器功能在执行之前,之后或过程中检查代码,并确定哪些代码是“热门”代码,例如可以使用JIT编译哪些结构。
当某个代码结构达到阈值时,跟踪器可以在运行代码时对其进行编译,这也是可配置的。
Tracing JIT 和 Function JIT
PHP 8.0添加了两种JIT操作模式。这是可以进一步自定义的,但是最显着的JIT功能类型是function和tracing。
Function JIT
相比之下,Function JIT模式是一个相当简单的模式。它通过JIT编译整个函数,而无需跟踪常用的代码结构,例如函数内部的循环。它仍然支持对常用函数进行性能分析,并在执行应用程序请求之前,之后或期间触发JIT编译或编译后的机器代码的执行。
Tracing JIT
Tracing JIT(在PHP 8.0中默认选择)试图识别经常使用的代码部分,并选择性地编译这些结构,以在编译时间和内存使用之间取得最佳平衡。并非所有的编程语言都支持tracing JIT编译器,但是PHP支持从第一个发行版开始就支持tracing JIT,并且默认情况下也是选中的。
有几个配置选项可以进一步调整如何确定热代码结构,例如函数调用的次数,循环结构的迭代次数等。
分析和优化
JIT可以在代码运行时对其进行检查,分析和优化。PHP JIT提供了对阈值和触发器的细粒度控制,其中涉及多少调用使其成为将JIT编译为机器代码的合适候选者,并且可以使用新编译的代码。如果在缓冲区中也存在后续的请求,则可以使用编译后的代码。
PHP的JIT实现允许微调何时应该使用JIT(何时加载脚本,第一次运行后或在执行期间),什么(整个函数或单个代码结构)以及如何进行优化(使用 AVX指令,CPU寄存器等)。
JIT友好代码
当JIT可以尽可能多地分流到本机CPU寄存器和指令时,它将受益匪浅。PHP是一种弱类型的语言,这使得很难推断变量的类型,并且需要对变量的生命周期进行更多分析,因为变量的类型可能在同一代码结构的稍后位置发生变化。
严格类型的代码以及具有标量类型的函数可以帮助JIT推断类型,并在可能的情况下利用CPU寄存器和专用指令。例如,一个纯函数(没有副作用),启用严格类型并带有参数和返回类型可能是理想的选择:
declare(strict_types=1);
function sum(float $a, float $b): float {
return $a + $b;
}
当PHP无法推断类型时,它可能无法充分利用JIT优化。
实际上,PHP 7的一些改进来自这些优化,它们可以消除无效代码并改善引用计数。这意味着更严格类型化的代码为PHP提供了更多机会来优化Opcache级别和JIT级别的代码。
受IO约束的应用程序(例如,广泛使用数据库的应用程序,DNS查询,文件读/写操作,FTP,套接字等)可能不会看到明显的差异,因为IO操作本身通常是这种应用的瓶颈。
基本的JIT配置
默认情况下,JIT是启用的,但可通过限制缓冲区大小将其关闭。
PHP JIT:
PHP 8.0的JIT基础概述和配置选项,请参阅PHP 8.0:JIT。
这篇文章是关于基准,JIT如何工作以及理想的配置选项的。
最简单的设置是简单地为JIT设置缓冲区大小,然后JIT将使用合理的默认值。
opcache.enable=1
opcache.enable_cli=1
opcache.jit_buffer_size=256M
这将为JIT缓冲区分配256 MB,并在CLI应用程序上启用JIT。
opcache.jit指令允许微调JIT功能。
opcache.jit=tracing
opcode.jit是有点复杂的配置值。它接受disable,on,off,trace,function,4 位值(4-digit)(不是位掩码),按顺序排列 4 个不同的标志。。
disable:在启动时完全禁用JIT功能,并且在运行时无法启用。
off:禁用,但是可以在运行时启用JIT。
on:启用tracing模式。
tracing:细化配置 的别名1254。
function:细化配置 的别名1205。
PHP JIT接受tracing或function作为表示配置组合的简单配置。
除tracing和function别名外,该opcache.jit伪指令还接受4位数字的配置值。它可以进一步配置JIT行为。
4位配置值的格式为CRTO,其中每个位置允许单个标记指定的字母数字值。
JIT标记
所述opcache.jit指令接受一个4位值以控制JIT行为,以CRTO的形式,并接受C,R,T,和O位置的以下值。
C:CPU特定的优化标志
0:禁用特定于CPU的优化。
1:如果CPU支持,则启用AVX。
R:寄存器分配
0:不执行寄存器分配。
1:执行块本地( block-local)寄存器分配。
2:执行全局寄存器分配。
T:触发器
0:在脚本加载时编译所有函数。
1:首次执行时编译所有函数。
2:剖析第一个请求,然后编译最热的函数。
3:即时分析并编译热函数。
4:当前未使用。
5:使用tracing JIT。动态分析并编译跟踪以查找热代码段。
O:优化级别
0:没有JIT。
1:最小JIT(调用标准VM处理程序)。
2:内联VM处理程序。
3:使用类型推断。
4:使用调用图。
5:优化整个脚本。
4触发器(T=4)下的选项未进入JIT实现的最终版本。它是使用@jitDocBlock注释属性声明的函数上的触发JIT 。现在未使用。
function与tracingJIT配置都利用CPU指令集和CPU寄存器分配来充分利用 CPU 功能(C = 1,R = 2)。
opcache.jit=function
function 是C = 1,R = 2,T = 0,O = 5的别名。
与function配置的区别在于,它渴望尽快编译脚本,然后编译整个脚本。这是一种更加冒昧和大胆的方法,类似于使用PHP 7.4中的预加载功能将PHP文件预加载到Opcache。
opcache.jit=tracing
tracing 是C = 1,R = 2,T = 5,O = 4的别名。
启用tracing后,JIT可以更精细,并选择函数中的代码段进行编译。理想的候选对象是循环结构和经常调用的函数。
这是默认配置,可以在性能优势和编译开销之间提供更多的平衡。
JIT tracing功能(T = 2、3或5)允许进一步调整将一个函数标记为hot并最终进行JIT编译需要多少次调用。
指令 描述 默认值
opcache.jit_hot_loop 经过几次迭代后,循环被认为很热。 64
opcache.jit_hot_func 在几次调用之后,一个函数被认为是热的。 127
opcache.jit_hot_return 在有多少返回后,返回被认为是热的。 8
opcache.jit_hot_side_exit 在有多少退出一侧后,退出被认为是热的。 8
默认值可能最适合几乎所有应用程序,降低默认值会降低阈值,从而导致要编译更多代码结构。
理想的JIT配置
更多的JIT编译代码并不一定意味着应用程序更快(如下面的Web应用程序基准所示)。由于花在JIT编译步骤上的时间,编译开销和较小的缓冲区会使应用程序变慢。
opcache.jit值最好保持不变(默认值为 tracing),因为它已经在CPU使用率,内存和跟踪编译哪些代码结构方面保持了良好的平衡。
对于大量IO绑定的应用程序,JIT将不会获得任何有意义的性能优势。实际上,当今的大多数Web应用程序都是IO大量的,JIT不会带来任何改变,更不用说是积极的了。
对于缓冲区大小,请注意不要有太小的内存,这会浪费JIT编译的代码并导致频繁的重新编译。太大的内存也可能是一个过大的杀伤力。Opcode的当前Opcache共享内存的50-100%可能是opcache.jit_buffer_size的理想值。
JIT基准
以下所有测试均在8核16线程x86-64系统上完成。但是,测试从不使用需要64位寄存器的整数,以使测试与x86 CPU更相关。
PHP脚本基准
PHP源代码包含两个基准脚本,用于测试各种PHP功能。micro_bench.php和bench.php文件已在PHP 8.0分支上进行了测试(该分支自PHP 8.0.0发行以来包含一些错误修复)。
第一次测试是在完全关闭Opcache的情况下进行的,第二个测试是在关闭JIT的情况下进行的,但启用了opcache。
两种JIT模式均会带来实质性的性能提升,但tracing模式稍有领先。
该基准几乎不能代表现实生活中的PHP应用程序。重复调用相同的函数,以及测试代码更简单,副作用更少的特性,使JIT受益匪浅。
PHP斐波那契基准
一个简单的斐波那契函数,用于计算斐波那契序列中的第42个数字。
Fibonacci序列全都与重复的函数调用有关,并且也不会说出真实PHP应用程序的完整故事,除非它当然是Fibonacci计算器应用程序。
斐波那契:PHP与其他语言
其他编译语言(例如Go,Rust和C)和Node JS也具有相同的Fibonacci(42)测试,后者也具有JIT功能。
PHP 8.0的JIT并未尝试其他AOT编译语言可以执行的所有可能的优化。但是,PHP 8.0的JIT带来了显着的性能提升,还有更多的改进余地。
Web应用基准
很难预测JIT的影响,因为JIT高度依赖强调工作负载。下面的大多数示例都是Web框架的hello-world示例,由于所涉及的各种插件和缓存系统,它们不一定代表实际使用情况。
使用数据库连接的应用程序在数据库查询时可能会遇到最大的瓶颈。在测量每秒请求数的Web服务器测试中,TLS,HTTP和FPM开销可能远远超过JIT造成的性能差异。
Laravel(8.4.4)和Symfony(演示版1.6.3,使用5.1.8版组件)及其骨架应用程序在与以前的基准测试相同的场景下,在相同的硬件上进行了测试。这两个应用程序均由内置的PHP Web服务器提供服务,并使用Apache Bench(ab)进行基准测试,并发率为5,并发请求为100。5次测试的平均值。
这两个应用程序都没有收到明显的收益,在Laravel中,JIT的性能降低了约2%,这可能是由于编译开销并未超过工作量。
对每个应用程序进行基准测试
为了获得真正的性能收益,每个应用程序都需要处于基准之下,以衡量使用JIT是否可以带来显着的收益。
CLI应用程序(尤其是CPU密集型应用程序)可能会获得实质性的性能提升。
对于诸如Composer和PHPUnit之类的网络和文件密集型应用程序,由于它们无法从机器代码改进中受益匪浅,因此不太可能获得性能提升。增加更多的SSD / RAM容量和带宽以获得更好的结果。
总结
JIT是使PHP性能更快并利用强调硬件功能的重要一步。这是多年的努力,并且已经显示出在计算密集型工作负载方面的实质性改进。
PHP的JIT仍有改进的余地,并且从现在开始可能只会变得更好。
非常感谢Dmitry Stogov和Nikita Popov在JIT方面所做的出色工作。Nikita还友好而迅速地回顾了本文的第一部分,介绍了JIT是如何工作的。谢谢❤🙏🏼。
评论 (0)