PHP 8 的 JIT(Just In Time)编译器将作为扩展集成到 php 中 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 代码(命令行脚本或者 WEB 应用)时,都要经过 PHP 解释器。最常用的是 PHP-FPM 和 CLI 解释器。
解释器的工作很简单:接收 PHP 代码,对其进行解释,然后返回结果。
一般的解释型语言都是这个流程。有些语言可能会减少几个步骤,但总体的思路相同。在 PHP 中,这个流程如下:
读取 PHP 代码并将其解释为一组称为 Tokens 的关键字。这个过程让解释器知道各个程序都写了哪些代码。 这一步称为 Lexing 或 Tokenizing 。
拿到 Tokens 集合以后,PHP 解释器将尝试解析他们。通过称之为 Parsing 的过程生成抽象语法树(AST)。这里 AST 是一个节点集表示要执行哪些操作。比如,「 echo 1 + 1 」实际含义是 「打印 1 + 1 的结果」 或者更详细的说 「打印一个操作,这个操作是 1 + 1」。
有了 AST ,可以更轻松地理解操作和优先级。将抽象语法树转换成可以被 CPU 执行的操作需要一个用于过渡的表达式 (IR),在 PHP 中我们称之为 Opcodes 。将 AST 转换为 Opcodes 的过程称为 compilation 。
有了 Opcodes ,有趣的部分就来了: executing 代码! PHP 有一个称为 Zend VM 的引擎,该引擎能够接收一系列 Opcodes 并执行它们。执行所有 Opcodes 后, Zend VM 就会将该程序终止。
一个简化版的 PHP 解释流程概述。
如你所见。这里有个问题:即使 PHP 代码没改变,每次执行还是会走此流程吗?
让我们看回 Opcodes 。对了!这就是 Opcache 扩展 存在的原因。
Opcache 扩展
Opcache 扩展是 PHP 附带的,通常没必要停用它。使用 PHP 最好打开 Opcache 。
它的作用是为 Opcodes 添加一个内存共享缓存层。它的工作是从 AST 中提取新生成的 Opcodes 并缓存它们,以便执行时
可以跳过 Lexing/Tokenizing 和 Parsing 步骤。
PHP 使用 Opcache 的解释流程。如果文件已经被解析,则 PHP 会为其获取缓存的 Opcodes ,而不是再次解析。
完美的跳过了 Lexing/Tokenizing 、 Parsing 和 Compiling 步骤 。
旁注: 这是超赞的 PHP 7.4 预加载功能 RFC ! 允许你告诉 PHP FPM 解析代码库,将其转换为 Opcodes 并且在执行之前就将其缓存。
Just In Time 编译有什么效果?
听了 Zeev 在 PHP Internals News 发表的 PHP 和 JIT 广播 之后,我弄清了 JIT 实际做了什么事情。
如果说 Opcache 扩展可以更快的获取 Opcodes 将其直接转到 Zend VM,则 JIT 让它们完全不使用 Zend VM 即可运行。
Zend VM 是用 C 编写的程序,充当 Opcodes 和 CPU 之间的一层。 JIT 在运行时直接生成编译后的代码,因此 PHP 可以
跳过 Zend VM 并直接被 CPU 执行。 从理论上说,性能会更好。
这听起来很奇怪,因为在编译成机器码之前,需要为每种类型的结构体编写一个具体的实现。但实际上这也是合理的。
PHP 的 JIT 使用了名为 DynASM (Dynamic Assembler) 的库,该库将一种特定格式的一组 CPU 指令映射为许多不同 CPU 类型的汇编代码。因此,编译器只需要使用 DynASM 就可以将 Opcodes 转换为特定结构体的机器码。
但是,有一个问题困扰了我很久。
如果预加载能够在执行之前将 PHP 代码解析为 Opcodes,并且 DynASM 可以将 Opcodes 编译为机器码 (Just In Time 编译) ,为什么我们不立即使用运行前编译 (Ahead of Time 编译) 立即编译 PHP 呢?
通过收听 Zeev 的广播,我找到的原因之一就是 PHP 是弱类型语言,这意味着在 Zend VM 尝试执行某个操作码之前, PHP 通常不知道变量的类型。
可以查看 Zend_value 联合类型 得知,很多指针指向不同类型的变量。每当 Zend VM 尝试从 Zend_value 获取值时,它都会使用像 ZSTR_VAL 这样的宏,获取联合类型中字符串的指针。
例如,这个 Zend VM handler 是处理「小于或等于」(<=) 表达式。看看它编码这么多的 if else 分支,只是为了类型推断。
使用机器码执行类型推断逻辑是不可行的,并且可能变得更慢。
先求值再编译也不是一个好选择,因为编译为机器码是 CPU 密集型任务。因此,在运行时编译所有内容也不好。
那么 Just In Time 编译是怎么做的?
现在我们知道无法很好的推断类型来提前编译。我们也知道在运行时进行编译的运算成本很高。那么 JIT 对 PHP 有何好处呢?
为了寻求平衡, PHP 的 JIT 尝试只编译有价值的 Opcodes 。为此, JIT 会分析 Zend VM 要执行的 Opcodes 并检查可能编译的地方。(根据配置文件)
当某个 Opcode 编译后,它将把执行交给该编译后的代码,而不是交给 Zend VM 。
PHP 的 JIT 解释流程。如果已编译,则 Opcodes 不会通过 Zend VM 执行。
因此,在 Opcache 扩展中,有两条检测指令判断要不要编译 Opcode 。如果要,编译器将使用 DynASM 将此 Opcode 转换为机器码,并执行此机器码。
有趣的是,由于当前接口中编译的代码有 MB 的限制 (也是可配置的),所以代码执行必须能够在 JIT 和解释代码之间无缝切换。
顺便说一句,Benoit Jacquemont 在 php 的 JIT 上的这篇演讲帮助我理解了这整件事。
我仍然不确定编译部分什么时候有效进行,但我想现在我真的不想知道。
所以你的性能收益可能不会很大
我希望现在大家都很清楚为什么大多数 php 应用程序不会因为使用即时编译器而获得很大的性能收益。这也是为什么 Zeev 建议为你的应用程序分析和试验不同的 JIT 配置是最好的方法。
如果您使用的是 PHP FPM,则通常会在多个请求之间共享已编译的操作码,但这仍然不能改变游戏规则。
这是因为 JIT 优化了计算密集型的操作,而如今大多数 php 应用程序比其他任何东西都更受 I/O 约束。如果您无论如何都要访问磁盘或网络,则处理操作是否已编译则无关紧要。时间上将非常相似。
除非…
你正在做一些不受 I/O 约束的事情, 像图像处理或机器学习。 任何不接触 I/O 的东西都将受益于 JIT 编译器。
这也是为什么现在人们说我们更愿意用 PHP 编写原生功能而不是 C 编写的原因。 如果仍然要编译此功能,则开销将毫无表现力。
https://www.cnblogs.com/a609251438/p/12980919.html
https://phpinternals.news/7
https://luajit.org/dynasm.html
https://afup.org/talks/3015-php-8-et-just-in-time-compilation
https://zhuanlan.zhihu.com/p/102278033
https://zhuanlan.zhihu.com/p/121762189
https://zhuanlan.zhihu.com/p/122398882
https://www.zhihu.com/question/319093848/answer/648577830
https://www.zhihu.com/question/316234502?sort=created
https://wiki.php.net/rfc/jit
PHP 实现了一个虚拟机 Zend VM,它会将人类可读脚本编译成虚拟机理解的指令,也就是操作码,这个执行阶段就是“编译时(Compile Time)”。在“运行时(Runtime)”执行阶段,虚拟机 Zend VM 会执行这些编译好的操作码。
通常编译时与运行时两个阶段是独立分开的,脚本编译完成后,像 APC 与 OPCache 这样的字节码缓存组件会缓存这些操作码。而 JIT 去掉了编译时阶段,它将这编译时与运行时两个阶段合为一体,实现即时编译与执行。
JIT 是一种编译器策略,它将代码表述为一种中间状态,在运行时将其转换为依赖于体系结构的机器码,并即时执行。在 PHP 中,这意味着 JIT 将为 Zend VM 生成的指令视为中间表述,并以依赖于体系结构的机器码执行,也就是说托管代码的不再是 Zend VM,而是更为底层的 CPU。
虽然自 PHP 7.0 以来,通过优化核心数据结构 HashTable、强化 Zend VM 中某些操作码与持续改进 OPCache 的 Optimizer 组件等具体措施,PHP 性能得到了显著提升,但是实际上这些优化似乎已经走到极限了。现在 JIT 从底层着手,被认为是目前提升 PHP 性能的最佳出路。
关于是否引入 JIT 的官方投票结果已于近日公布,因为大部分核心开发者投了赞成票,所以 PHP 8 中将会支持 JIT。
另外值得一提的是,PHP JIT 对于使用 PHP 的网站来说提速可能并不明显,因为 JIT 在 CPU 密集型的代码上效果最好,而一般情况下,用 PHP 编写的程序都是 I/O 密集型的。简单来说就是,PHP 程序往往受限于 I/O 而不是 CPU,使 PHP 代码运行速度变慢的因素往往是它们正在执行的 I/O 操作,包括连接、读取和写入数据库、高速缓存、文件与套接字等。
PHP 中 CPU 密集型代码的一个例子是 Zend/bench.php。
那么 PHP 中的 JIT 将会在哪里发挥作用呢?答案是数学领域。关于 PHP JIT 的详细介绍,可以查看:https://blog.krakjoe.ninja/2019/03/php-gr8.html
https://www.oschina.net/news/105566/php-8-will-support-jit
HP 8将正式引入JIT(Just-In- Time 即时编译) 支持。引入JIT支持将进一步提升PHP性能,JIT编译器被视为PHP版本中下一个主要的进步。今年PHP 7.4版本将发布,消息称PHP 8可能要等到2021年底。
JIT 并非是新技术,很多语言例如 Java 早已实现。JIT 的思想很简单,即在程序运行时动态对程序进行编译,生成平台相关的机器码(比如运行它的机器 CPU 的本地代码),从而加快程序的运行速度。此前,PHP的JIT支持已经通过一个独立的分支进行了一段时间的开发,投票结束后,将正式合并到PHP 8中。
在PHP中,JIT将为Zend Vm生成的指令作为中间表示,并发出依赖于体系结构的机器代码,这样代码的宿主就不再是Zend Vm,而是直接由CPU发出。
在PHP 7.0之前,由于Facebook旗下的HHVM(Facebook开源的PHP执行引擎)项目带来的竞争,PHP社区内部关注的焦点一直是性能。PHP 7.0中的大多数核心更改都包含在PHPNG补丁中,该补丁显著改进了PHP在其核心上使用内存和CPU的方式。
自PHP 7.0以来,陆续发布了多个更新以优化性能,但PHP的性能已经逐渐逼近天花板。因此,引入JIT支持将为PHP释放更多性能。
但PHP官方博客同样提到:JIT可能不会让网站运行更快,因为在一般情况下,用PHP编写的应用程序是I/O绑定的,而JIT在CPU绑定的代码上工作得最好。
https://www.oschina.net/news/80040/php-5-to-7-performance-evaluation
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解释流程的简化概述。
如您所见,很简单。但是这里有一个瓶颈:如果您的php代码可能不会经常更改,那么每次执行代码时对其进行词法分析和解析有什么意义?
最后,我们只关心操作码,对吗?对!这就是存在Opcache扩展的原因。
Opcache扩展
Opcache扩展是PHP附带的,通常没有太大的理由要停用它。如果使用PHP,则可能应该打开Opcache。
它的作用是为操作码添加一个内存共享缓存层。它的工作是从AST中提取新生成的操作码并将其缓存,以便进一步执行可以轻松地跳过词法分析和语法分析阶段。
这是考虑了Opcache扩展的流程示意图:
PHP使用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的解释流程
PHP的JIT解释流程。如果已编译,则操作码不会通过Zend VM执行。
因此,在Opcache扩展中,有两条指令可检测是否应编译某个Opcode。如果是,则编译器然后使用DynASM将此操作码转换为机器代码,并执行此新生成的机器代码。
有趣的是,由于当前实现中已编译的代码以兆字节为单位(也是可配置的),因此代码执行必须能够在JIT和解释的代码之间无缝切换。
顺便说一下,来自Benoit Jacquemont的有关php JIT的演讲帮助我了解了很多事情。
我仍不确定编译部分何时有效进行,但我想我现在暂时不想知道。
因此,您的性能提升可能不会很大
我希望现在更加清楚,为什么每个人都在说大多数php应用程序不会因为使用Just In Time编译器而获得巨大的性能优势。为什么Zeev建议为您的应用程序分析和试验不同的JIT配置是最好的方法。
如果使用PHP FPM,通常将在多个请求之间共享已编译的操作码,但这仍然不能改变游戏规则。
这是因为JIT优化了CPU约束的操作,并且当今大多数php应用程序都比任何东西受I / O约束更多。不管是否要访问磁盘或网络,处理操作是否已编译都没有关系。时间将非常相似。
除非…
您正在执行不受I / O约束的操作,例如图像处理或机器学习。任何不接触I / O的东西都将从“即时编译器”中受益。
这也是为什么人们现在说我们更愿意编写用PHP而不是C编写的本机PHP函数的原因。如果仍然编译此类函数,则开销将无法表达。
https://www.debug8.com/php/t_37378.html
https://baijiahao.baidu.com/s?id=1629602566470825101&wfr=spider&for=pc
https://zhuanlan.zhihu.com/p/144207798
https://www.php.cn/php-weizijiaocheng-453225.html