【PHP8.0】PHP 将支持 JIT 编译技术
2020/06/26附言:由于Alpha版已发布,我尝试了实际使用。
JIT的RFC于2019年3月21日开始投票。
截止日期为2019年3月28日,但截至2019年3月27日,赞成票为48票,反对票为2票,几乎已确定将被采纳。
即时编译 (JIT) 是一种技术。
JIT 是什么?
目前,PHP是一种解释型的编程语言,每次接收请求时会读取全部源代码,将其转换为opcode,然后按顺序逐一执行,执行完成后将丢弃所有代码。虽然说它速度较慢,但从执行内容来看,其速度实际上是异常地快。
无论是CPU、操作系统还是其他执行环境,opcode都会生成相同的代码。
当按顺序执行时,会将其转换为特定执行环境的本地代码并执行。
OPcache会将转换后的opcode存储在内存中,以便在下一次请求中重复使用。
JIT已经进一步发展,当请求到来时,它会读取源代码并转换为opcode,但之后会立即将其转换为本机代码。
当再次调用相同的处理时,通过直接执行本机代码,处理速度会极快。
此外,还可以将这些本机代码保存在内存中以便重复使用。
然而,由于本地代码是CPU直接执行的代码,因此它会因CPU类型和代际而异。根据执行环境需要创建不同的本地代码,这就需要进行大规模的修改,代码也会变得非常复杂。而这种大规模的改动是什么程度的呢?就像让人晕眩的900次提交和5万行的添加量一样。
简而言之,是什么意思?
OPcache真厉害。
即时编译请求信息。
介绍
原本打算在PHP7中实现JIT,并且自2011年起Zend(主要是Dmitry)就一直在尝试各种方法,但由于种种原因,最终没有将其纳入PHP7。原因是,当时的方法并没有显著提高性能,反而增加了复杂性,而且还引入了很多除了JIT之外的性能改进技术。
事实上,PHP7进行了令人惊讶的高速化改进,处理时间比PHP5减少了一半以下。当时由于专注于这些加速改进,没有引入JIT。
如今对于JIT的案例
当前在PHP中引入JIT有以下预期优点。
首先,除了JIT之外的优化策略正在逐渐达到极限。
换句话说,如果不使用JIT,就无法实现更高的加速。
下一步,通过引入JIT,将PHP用于编写需要大量使用Web以外的CPU进行处理的选择将变得更有力。
最後,你可以选择在PHP中开发内建函数,而不是选择C语言或者用C语言作为替代。这将基本上不会受到当前PHP中采用此策略所面临的性能下降问题的影响。
此外,如果基于PHP开发,语言级别将安全保护你免受使用C语言开发常常出现的内存管理和溢出等问题的困扰。
提案 (Tí’àn)
我们在PHP8中提供即时编译(JIT)功能。
在其中PHP JIT成为OPcache的一部分,但几乎是作为独立实现。在PHP编译时设置其有效或无效。如果启用,则PHP文件的本地代码将被保存到OPCache的共享内存中。
生成原生代码时,使用DynAsm工具。这是一个由LuaJIT项目开发的非常轻量级和高级的工具。然而,同时需要对目标汇编语言有高级的理解。曾经尝试过LLVM,但是代码生成速度慢了100倍,无法使用。DynAsm在POSIX和Windows上支持x86、x86_64和ARM。因此,应该能够支持当前PHP所支持的常见平台。只要努力。
此外,我在这里提到了一些关于内部实现的内容,但是我并不太理解。比如说没有对应additional IR form,或者opcache optimizer的SSA静态分析框架正在生成本地代码,或者说当转化为long型后会直接注册到CPU寄存器而不是内存中,还有PHP JIT的寄存器分配算法非常厉害之类的,或许有这样的内容,又或许没有,详细情况请参考@sj-i先生的评论。
演出
这个函数的基准测试已经公开发布了。
function iterate($x,$y){
$cr = $y-0.5;
$ci = $x;
$zr = 0.0;
$zi = 0.0;
$i = 0;
while (true) {
$i++;
$temp = $zr * $zi;
$zr2 = $zr * $zr;
$zi2 = $zi * $zi;
$zr = $zr2 - $zi2 + $cr;
$zi = $temp + $temp + $ci;
if ($zi2 + $zr2 > BAILOUT)
return $i;
if ($i > MAX_ITERATIONS)
return 0;
}
}
执行结果如下。
虽然版本不明,但原生的PHP7速度超过了20倍,比HHVM快3倍,也比gcc和LuaJIT快,这确实让人怀疑是否有什么错误。
顺便说一句,这个长椅是4年前的。
根据RFC,在没有JIT的PHP7.4中,执行时间是0.046秒。
现在PHP7.4只需0.046秒,而PHP7需要0.281秒,我觉得在这个时点已经很奇怪了。
这只是执行环境的不同造成的吗?
此外,我还注意到,如果仔细观察,只有PHP使用了ob_start/ob_end_flush来抑制输出,这让我感到有点担忧。虽然这对PHP之间的比较没有影响,但是与其他语言的比较可能不公平。
顺便说一下,据说用Go并行处理只需要0.002秒。哇哦。
sub $0x10, %esp
cmp $0x1, 0x1c(%esi)
jb .L14
jmp .L1
.ENTRY1:
sub $0x10, %esp
.L1:
cmp $0x2, 0x1c(%esi)
jb .L15
mov $0xec3800f0, %edi
jmp .L2
.ENTRY2:
sub $0x10, %esp
.L2:
cmp $0x5, 0x48(%esi)
jnz .L16
vmovsd 0x40(%esi), %xmm1
vsubsd 0xec380068, %xmm1, %xmm1
.L3:
mov 0x30(%esi), %eax
mov 0x34(%esi), %edx
mov %eax, 0x60(%esi)
mov %edx, 0x64(%esi)
mov 0x38(%esi), %edx
mov %edx, 0x68(%esi)
test $0x1, %dh
jz .L4
add $0x1, (%eax)
.L4:
vxorps %xmm2, %xmm2, %xmm2
vxorps %xmm3, %xmm3, %xmm3
xor %edx, %edx
.L5:
cmp $0x0, EG(vm_interrupt)
jnz .L18
add $0x1, %edx
vmulsd %xmm3, %xmm2, %xmm4
vmulsd %xmm2, %xmm2, %xmm5
vmulsd %xmm3, %xmm3, %xmm6
vsubsd %xmm6, %xmm5, %xmm7
vaddsd %xmm7, %xmm1, %xmm2
vaddsd %xmm4, %xmm4, %xmm4
cmp $0x5, 0x68(%esi)
jnz .L19
vaddsd 0x60(%esi), %xmm4, %xmm3
.L6:
vaddsd %xmm5, %xmm6, %xmm6
vucomisd 0xec3800a8, %xmm6
jp .L13
jbe .L13
mov 0x8(%esi), %ecx
test %ecx, %ecx
jz .L7
mov %edx, (%ecx)
mov $0x4, 0x8(%ecx)
.L7:
test $0x1, 0x39(%esi)
jnz .L21
.L8:
test $0x1, 0x49(%esi)
jnz .L23
.L9:
test $0x1, 0x69(%esi)
jnz .L25
.L10:
movzx 0x1a(%esi), %ecx
test $0x496, %ecx
jnz JIT$$leave_function
mov 0x20(%esi), %eax
mov %eax, EG(current_execute_data)
test $0x40, %ecx
jz .L12
mov 0x10(%esi), %eax
sub $0x1, (%eax)
jnz .L11
mov %eax, %ecx
call zend_objects_store_del
jmp .L12
.L11:
mov 0x4(%eax), %ecx
and $0xfffffc10, %ecx
cmp $0x10, %ecx
jnz .L12
mov %eax, %ecx
call gc_possible_root
.L12:
mov %esi, EG(vm_stack_top)
mov 0x20(%esi), %esi
cmp $0x0, EG(exception)
mov (%esi), %edi
jnz JIT$$leave_throw
add $0x1c, %edi
add $0x10, %esp
jmp (%edi)
.L13:
cmp $0x3e8, %edx
jle .L5
mov 0x8(%esi), %ecx
test %ecx, %ecx
jz .L7
mov $0x0, (%ecx)
mov $0x4, 0x8(%ecx)
jmp .L7
.L14:
mov %edi, (%esi)
mov %esi, %ecx
call zend_missing_arg_error
jmp JIT$$exception_handler
.L15:
mov %edi, (%esi)
mov %esi, %ecx
call zend_missing_arg_error
jmp JIT$$exception_handler
.L16:
cmp $0x4, 0x48(%esi)
jnz .L17
vcvtsi2sd 0x40(%esi), %xmm1, %xmm1
vsubsd 0xec380068, %xmm1, %xmm1
jmp .L3
.L17:
mov %edi, (%esi)
lea 0x50(%esi), %ecx
lea 0x40(%esi), %edx
sub $0xc, %esp
push $0xec380068
call sub_function
add $0xc, %esp
cmp $0x0, EG(exception)
jnz JIT$$exception_handler
vmovsd 0x50(%esi), %xmm1
jmp .L3
.L18:
mov $0xec38017c, %edi
jmp JIT$$interrupt_handler
.L19:
cmp $0x4, 0x68(%esi)
jnz .L20
vcvtsi2sd 0x60(%esi), %xmm3, %xmm3
vaddsd %xmm4, %xmm3, %xmm3
jmp .L6
.L20:
mov $0xec380240, (%esi)
lea 0x80(%esi), %ecx
vmovsd %xmm4, 0xe0(%esi)
mov $0x5, 0xe8(%esi)
lea 0xe0(%esi), %edx
sub $0xc, %esp
lea 0x60(%esi), %eax
push %eax
call add_function
add $0xc, %esp
cmp $0x0, EG(exception)
jnz JIT$$exception_handler
vmovsd 0x80(%esi), %xmm3
jmp .L6
.L21:
mov 0x30(%esi), %ecx
sub $0x1, (%ecx)
jnz .L22
mov $0x1, 0x38(%esi)
mov $0xec3802b0, (%esi)
call rc_dtor_func
jmp .L8
.L22:
mov 0x4(%ecx), %eax
and $0xfffffc10, %eax
cmp $0x10, %eax
jnz .L8
call gc_possible_root
jmp .L8
.L23:
mov 0x40(%esi), %ecx
sub $0x1, (%ecx)
jnz .L24
mov $0x1, 0x48(%esi)
mov $0xec3802b0, (%esi)
call rc_dtor_func
jmp .L9
.L24:
mov 0x4(%ecx), %eax
and $0xfffffc10, %eax
cmp $0x10, %eax
jnz .L9
call gc_possible_root
jmp .L9
.L25:
mov 0x60(%esi), %ecx
sub $0x1, (%ecx)
jnz .L26
mov $0x1, 0x68(%esi)
mov $0xec3802b0, (%esi)
call rc_dtor_func
jmp .L10
.L26:
mov 0x4(%ecx), %eax
and $0xfffffc10, %eax
cmp $0x10, %eax
jnz .L10
call gc_possible_root
jmp .L10
后向兼容性
没有破坏互换性的变更。
其他影响
浏览器插件
像 Xdebug、XHProf、Blackfire、Tideways 这样的调试器和性能分析工具会受到影响。
Opcache (OP代码缓存)
即时编译(Just-In-Time Compilation)作为Opcache的一项功能被实现。
追加的常数
没有额外的常数被添加。
调试
调试JIT真的很困难啊!加油吧!
PHP配置文件
php.ini会被添加多个项目。
opcache.jit_buffer_size (PHP opcode cache’s Just-in-Time compilation buffer size) 可以进行改写为:opcache.jit_buffer_size(PHP的opcode缓存的即时编译缓冲大小)。
为本地代码保留的内存大小,以字节为单位,对应K和M的表示。
默认为0,表示禁用JIT。
opcache.jit:
OPcache即时编译
JIT的控制选项,按顺序表示为CRTO, 默认设置为”1205″。也许更改为”1235″可能会更好。
请用中文将以下内容表达,只需要一个选项:
C
CPU优化级别,范围为0-1。
0表示不使用,1表示启用AVX指令集。
以下是原文的中文释义:
R
寄存器分配,范围从0到2。
0代表不使用寄存器分配,1代表局部寄存器分配,2代表全局寄存器分配。
请用中文更加本地化地翻译以下内容,只需要一种选项:
T
JIT的启动时机和范围是0-5。
0表示在最初脚本启动时启用所有功能。
1表示在最初处理执行时启用JIT。
2表示在第一个请求中进行配置文件,然后在第二个请求中进行编译。
3表示即时进行配置文件和编译。
4表示编译带有”@jit”注释的函数。
只需要一种选项,将以下内容以中文进行翻译:
O
只需要一种选择。
最佳化级别,范围是0-5。
0表示不使用即时编译(JIT),5表示进行最高级别的优化。
缓存加速器.jit_debug
即时编译调试控制选项。
默认为0。
据说可以通过指定位来输出各种调试信息,但具体是什么我不太清楚。
似乎可以输出SSA表达式形式,perf.map以及即时编译后的代码。
表演
bench.php的执行时间从0.320秒减少到0.140秒。
对于使用CPU较多的处理,可以预期到戏剧性的加速。
此外,根据Nikita的说法,PHP-Parser的速度提高了1.3倍。
然而,像WordPress这样的Web应用程序并不能期望获得太多好处。
似乎只提高了从315req/秒到326req/秒的程度。
对于这种现实应用程序,我们计划进行进一步的改进,以提高速度。
未来的前景
计划通过生成经过优化的代码来改进JIT,这是在函数的分析之后进行的。
此外,还可以更深入地与预加载和FFI进行整合。
还可以期望标准化在PHP中提供用于编写内建函数而非C的方法。
投票 – (vote)
2019年3月21日开始投票,2019年3月28日投票结束。
要通过决议,需要投票者中有三分之二加一的赞成票。
PHP7.4
我没能进入PHP7.4。
虽然我已经创建了分支,但是对于引入到7.4版本,赞成的只有18人,反对的有34人,所以被否决了。
因为7.4本就是功能繁多的新版本,所以非常困难。
再加上添加5万行代码之类的任务,确实太严峻了。
这是一个外部连接。
请以中文原生语言解释以下内容,仅需要一个版本:
拉取请求 / 即时编译分支
这个提交有900个提交,并且添加了超过5万行代码,是一个非常庞大的拉取请求。
我绝对不想自己做这样的合并工作。
PHP 7.4分支
不太可能会使用PHP7.4的JIT分支。
DynASM / 非官方DynASM文件.
这是一个用于JIT的库。
【RFC】 【VOTE】即时编译
大家的感觉是“7.4是不可能的,应该只选择8”。
虽然这次改修规模如此之大,但帖子的回复并没有想象中的多,这是否意味着除了目标版本之外已经无话可说了,如同事先和预期一样?
印象
只要是正常制作Web应用程序,似乎没有什么影响。
JIT真正发挥作用的地方是在后台批处理等后端处理方面。
以前我们会认为用PHP进行批处理是“疯了吗!?”的印象,但未来PHP似乎比用其他语言编写更快。
还有,由于对opcache和JIT的理解有些模糊,所以可能存在许多错误。我相信一定会有人提出pull request来帮忙改正。