PHP 8 确认支持 JIT

介绍

众所周知,PHP 7的性能跳跃最初是由尝试为PHP实现JIT而启动的。我们在2011年开始在Zend(主要是由Dmitry)开展这些工作,从那时起尝试了3种不同的实现。我们从未提出过发布其中任何一个的建议,主要有三个原因:它们导致典型的Web应用程序没有实质性的性能提升; 它们的开发和维护非常复杂; 我们仍然有其他方向可以探索以提高性能,而无需使用JIT。

今日JIT的案例

尽管支持JIT的PHP的大部分基础都没有改变 - 我们相信今天有一个很好的案例可用于支持JIT的PHP。

首先,我们相信我们已经达到了使用其他优化策略提高PHP性能的能力。换句话说 - 除非我们使用JIT,否则我们无法进一步提高PHP的性能。

其次 - 使用JIT可能会为在其他非Web,CPU密集型场景中更频繁使用PHP打开大门 - 其中性能优势实际上非常可观 - 而且今天可能还没有考虑PHP。

最后 - 使JIT可用可以为我们(额外的努力)提供在PHP中开发内置函数的能力,而不是(或除了)C - 而不会遭受与此类策略相关的巨大性能损失。今天的非JITted发动机。反过来,这可以为更快的创新打开大门 - 而且还可以实现更安全的实施,这将不太容易受到内存管理,溢出和与基于C的开发相关的类似问题的影响。

提案

我们建议将JIT包含在PHP 8中,并提供额外的工作来提高其性能和可用性。

此外,我们建议考虑在PHP 7.4中包含JIT作为实验性功能(默认情况下禁用)。

PHP JIT实现为OPcache的几乎独立部分。它可以在PHP编译时和运行时启用/禁用。启用时,PHP文件的本机代码存储在OPcache共享内存的附加区域中,op_array→opcodes []。处理程序保留指向JIT编码的入口点的指针。这种方法根本不需要引擎修改。

我们使用DynAsm(为LuaJIT项目开发)来生成本机代码。它是一个非常轻量级和高级的工具,但它确实承担了目标汇编语言的良好和非常低级的开发知识。在过去,我们尝试过LLVM,但它的代码生成速度几乎慢了100倍,因此使用起来非常昂贵。目前,我们在POSIX平台和Windows上支持x86和x86_64 CPU。DynAsm还支持ARM。ARM64,MIPS,MIPS64和PPC,理论上我们应该能够支持PHP部署中常用的所有平台(给予足够的努力)。

PHP JIT不引入任何其他IR(中间表示)表单。它直接从PHP字节代码和SSA静态分析框架(opcache优化器的一部分)收集的信息生成本机代码。通常为每个PHP字节码指令单独生成代码。只考虑几种组合(例如比较+条件跳转)。

如果PHP变量的类型(在SSA中)被精确推断为LONG或DOUBLE,并且无法间接访问,则JIT可以将其值直接存储在CPU寄存器中,从而避免存储器和负载。PHP JIT线性扫描寄存器分配算法,结合高速和合理的质量。

JIT的质量可以在https://gist.github.com/dstogov/12323ad13d3240aee8f1上发布的Mandelbrot基准测试中得到证明,它可以将性能提高4倍以上(0.011秒对PHP 7.4的0.046秒)。

函数迭代($ 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 )
                返回 $ i ; 
            if  ($ i  > MAX_ITERATIONS )
                返回 0 ; 
        }
 
    }
以下是为上面的PHP函数生成的完整汇编代码,主循环代码在.L5和.L7之间可见:

JIT $ Mandelbrot :: iterate :  ; (/home/dmitry/php/bench/b.php)
	子 $ 0×10 , %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 )
	测试 $ 0x1 , %dh 
	jz 。L4
	 添加 $ 0x1 , (%eax )
。L4 :
	vxorps  %xmm2 , %xmm2 , %xmm2 
	vxorps  %xmm3 , %xmm3 , %xmm3 
	xor  %edx , %edx 
。L5 :
	cmp  $ 0x0 , EG ( vm_interrupt )
	jnz  。L18
	 加 $ 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 :
	测试 $ 为0x1 , 0x39 (%ESI )
	JNZ  。L21
 。L8 :
	测试 $ 为0x1 , ×49 (%ESI )
	JNZ  。L23
。L9 :
	测试 $ 为0x1 , 0×69 (%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 )
	测试 $ 0x40 , %ecx 
	jz  。L12
	 mov  0x10 (%esi ), %eax 
	sub  $ 0x1 , (%eax )
	jnz  。L11
	 mov  %eax , %ecx 
	调用 zend_objects_store_del
	 jmp  。L12
 。L11 :
	mov  0x4 (%eax ), %ecx 
	和 $0xfffffc10 , %ecx 
	cmp  $ 0x10 , %ecx 
	jnz  。L12
	 mov  %eax , %ecx 
	调用 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
	 添加 $ 0x1c , %edi 
	添加 $ 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 
	调用 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 )
	调用 rc_dtor_func
	 JMP  。L8
 。L22 :
	mov  0x4 (%ecx ), %eax 
	和 $0xfffffc10 , %eax 
	cmp  $ 0x10 , %eax 
	jnz  。L8
	 调用 gc_possible_root
	 jmp  。L8
 。L23 :
	mov  0x40 (%esi ), %ecx 
	sub  $ 0x1 , (%ecx )
	jnz  。L24
	 MOV  $ 为0x1 , 0x48 (%ESI )
	MOV  $ 0xec3802b0 , (%esi )
	调用 rc_dtor_func
	 jmp  。L9
 。L24 :
	mov  0x4 (%ecx ), %eax 
	和 $ 0xfffffc10 , %eax 
	cmp  $ 0x10 , %eax 
	jnz  。L9
	 调用 gc_possible_root
	 jmp  。L9
 。L25 :
	mov  0x60 (%esi ), %ecx 
	sub $ 0x1 , (%ecx )
	jnz  。L26
	 MOV  $ 为0x1 , 0x68 (%ESI )
	MOV  $ 0xec3802b0 , (%ESI )
	调用 rc_dtor_func
	 JMP  。L10
 。L26 :
	mov  0x4 (%ecx ), %eax 
	和 $ 0xfffffc10 , %eax 
	cmp  $ 0x10 , %eax 
	jnz  。L10
	 调用 gc_possible_root
	 jmp  。L10

与V8,HHVM,PyPy和其他大多数现代JIT实现相比,PHP JIT非常简单,但无论如何它增加了整个PHP复杂性的水平,新类型错误的风险以及开发和维护的成本。

向后不兼容的变化

没有

建议的PHP版本

PHP 8和PHP 7.4

到现有扩展

JIT将影响第三方调试器(例如xdebug)和分析器(例如XHProf,Blackfire,Tideways)。

对于调试特定请求,可以在RINIT阶段通过C API(zend_alter_ini_entry)禁用JIT(与opcache一起)更改“opcache.enable” 

运行时分析甚至可以使用JIT代码,但这可能需要开发额外的跟踪API和相应的JIT扩展,以生成跟踪回调。

对Opcache

JIT将作为OPcache的一部分实现。

新常数

没有

php.ini默认值

如果有任何php.ini设置,则列出:

  • opcache.jit_buffer_size - 为本机代码生成保留的共享内存缓冲区的大小(以字节为单位;支持K,M - 后缀)。默认值 - 0禁用JIT。
  • opcache.jit - JIT控件选项。由4位十进制数字组成 - CRTO(默认为1205.可能最好更改为1235)。
    • O - 优化级别
      • 0 - 不要JIT
      • 1 - 最小JIT(调用标准VM处理程序)
      • 2 - 选择性VM处理程序内联
      • 3 - 基于单个函数的静态类型推断的优化JIT
      • 4 - 基于静态类型推断和调用树的优化JIT
      • 5 - 基于静态类型推断和内部过程分析的优化JIT
    • T - JIT触发器
      • 0 - JIT在第一个脚本加载时的所有函数
      • 1 - 首次执行时的JIT功能
      • 2 - 第一次请求时的配置文件,第二次请求时编译热门功能
      • 3 - 动态配置文件并编译热门功能
      • 4 - 在doc-comments中使用@jit标记编译函数
    • R - 寄存器分配
      • 0 - 不执行寄存器分配
      • 1 - 使用本地线性扫描寄存器分配器
      • 2 - 使用全局线性扫描寄存器分配器
    • C - CPU特定的优化标志
      • 0 - 没有
      • 1 - 启用AVX指令生成
  • opcache.jit_debug - JIT调试控制选项,其中每个位启用一些调试选项。默认 - 0。
    • (1«0) - 打印生成的汇编代码
    • (1«1) - 用于代码生成的打印中间SSA表单
    • (1«2) - 注册分配信息
    • (1«3) - 打印存根汇编程序代码
    • (1«4) - 生成perf.map文件以列出Linux perf报告中的JIt-ed函数
    • (1«5) - 生成perf.dump文件以在Linux perf peport中显示JIT-ed函数的汇编代码
    • (1«6) - 提供有关Linux Oprofile的JIt-ed代码的信息
    • (1«7) - 提供有关英特尔VTune的JIt-ed代码的信息
    • (1«8) - 允许使用GDB调试JIT编码

性能

JIT使bench.php的速度提高了两倍多:0.140秒vs 0.320秒。预计这将使大多数CPU密集型工作负载运行得更快。

根据Nikita的说法,使用JIT,PHP-Parser的速度提高了约1.3倍。Amphp hello-world.php仅获得5%的加速。

然而,就像以前的尝试一样 - 它目前似乎没有显着改善像WordPress这样的现实应用程序(opcache.jit = 1235 326 req / sec vs 315 req / sec)。

它计划通过分析和推测优化来提供额外的工作,改进现实应用程序的JIT。

您可能还会对下面的文章感兴趣: