[转载] PHP 8新特性之JIT简介

转载自鸟哥博客 , 原文地址: https://www.laruence.com/2020/06/27/5963.htmlphp

PHP8 alpha1已经在昨天发布,相信关于JIT是你们最关心的,它到底怎么用,有什么要注意的,以及性能提高到底咋样?html

首先,咱们来看一张图:数据结构

左图是PHP8以前的Opcache流程示意图(zend引擎每次都先解释后执行), 右图是PHP8中的Opcache示意图(zend引擎直接执行机器码), 能够看出几个关键点:架构

  • Opcache会作opcode层面的优化,好比图中的俩条opcode合并为一条
  • PHP8的JIT目前是在Opcache之中提供的
  • JIT在Opcache优化以后的基础上,结合Runtime的信息再次优化,直接生成机器码
  • JIT不是原来Opcache优化的替代,是加强
  • 目前PHP8只支持x86架构的CPU ( 我在编译的时候有发现 )

事实上JIT共用了不少原来Opcache作优化的基础数据结构,好比data flow graph, call graph, SSA等,关于这部分,后续若是有时间,能够单独在写一个文章来介绍,今天就只是着重在使用层面。函数

下载安装好之后,除掉原有的opcache配置之外,对于JIT咱们须要添加以下配置到php.ini:oop

opcache.jit=1205
opcache.jit_buffer_size=64M

opcache.jit这个配置看起来稍微有点复杂,我来解释下, 这个配置由4个独立的数字组成,从左到右分别是(请注意,这个是基于目前alpha1的版本设置,一些配置可能会随着后续版本作微调):性能

  • 第一个数字是否在生成机器码点时候使用AVX指令, 须要CPU支持
0: 不使用
1: 使用

    第二个数字寄存器分配策略:学习

0: 不使用寄存器分配
1: 局部(block)域分配
2: 全局(function)域分配

  第三个数字是JIT触发策略测试

0: PHP脚本载入的时候就JIT
1: 当函数第一次被执行时JIT
2: 在一次运行后,JIT调用次数最多的百分之(opcache.prof_threshold * 100)的函数
3: 当函数/方法执行超过N(N和opcache.jit_hot_func相关)次之后JIT
4: 当函数方法的注释中含有@jit的时候对它进行JIT
5: 当一个Trace执行超过N次(和opcache.jit_hot_loop, jit_hot_return等有关)之后JIT

  第四个数字是JIT优化策略,数值越大优化力度越大优化

0: 不JIT
1: 作opline之间的跳转部分的JIT
2: 内敛opcode handler调用
3: 基于类型推断作函数级别的JIT
4: 基于类型推断,过程调用图作函数级别JIT
5: 基于类型推断,过程调用图作脚本级别的JIT

基于此,咱们能够大概获得以下几个结论:

  • opcache.jit的配置项尽可能使用12x5型的配置,此时应该是效果最优的
  • 对于x, 若是是脚本级别的,推荐使用0, 若是是Web服务型的,能够根据测试结果选择3或5
  • @jit的形式,在有了attributes之后,可能变为<<jit>>

如今,咱们来测试下启用和不启用JIT的时候,Zend/bench.php的差别,首先是不启用(php -d opcache.jit_buffer_size=0 Zend/bench.php):

不启用的结果:

simple             0.008
simplecall         0.004
simpleucall        0.004
simpleudcall       0.004
mandel             0.035
mandel2            0.055
ackermann(7)       0.020
ary(50000)         0.004
ary2(50000)        0.003
ary3(2000)         0.048
fibo(30)           0.084
hash1(50000)       0.013
hash2(500)         0.010
heapsort(20000)    0.027
matrix(20)         0.026
nestedloop(12)     0.023
sieve(30)          0.013
strcat(200000)     0.006
------------------------
Total              0.387

根据上面的介绍,咱们选择opcache.jit=1205, 由于bench.php是脚本(php -d opcache.jit_buffer_size=64M -d opcache.jit=1205 Zend/bench.php):

启用的结果是:

simple             0.002
simplecall         0.001
simpleucall        0.001
simpleudcall       0.001
mandel             0.010
mandel2            0.011
ackermann(7)       0.010
ary(50000)         0.003
ary2(50000)        0.002
ary3(2000)         0.018
fibo(30)           0.031
hash1(50000)       0.011
hash2(500)         0.008
heapsort(20000)    0.014
matrix(20)         0.015
nestedloop(12)     0.011
sieve(30)          0.005
strcat(200000)     0.004
------------------------
Total              0.157

可见,对于Zend/bench.php, 相比不开启JIT,开启了之后,耗时下降将近60%,性能提高将近2倍。

对于你们研究学习来讲,能够经过opcache.jit_debug来观测JIT后生成的汇编结果,好比对于:

function simple() {
  $a = 0;
  for ($i = 0; $i < 1000000; $i++)
    $a++;
}

咱们经过php -d opcache.jit=1205 -dopcache.jit_debug=0x01 能够看到:

JIT$simple: ; (/tmp/1.php)
     sub $0x10, %rsp
     xor %rdx, %rdx
     jmp .L2
.L1:
     add $0x1, %rdx
.L2:
     cmp $0x0, EG(vm_interrupt)
     jnz .L4
     cmp $0xf4240, %rdx
     jl .L1
     mov 0x10(%r14), %rcx
     test %rcx, %rcx
     jz .L3
     mov $0x1, 0x8(%rcx)
.L3:
     mov 0x30(%r14), %rax
     mov %rax, EG(current_execute_data)
     mov 0x28(%r14), %edi
     test $0x9e0000, %edi
     jnz JIT$$leave_function
     mov %r14, EG(vm_stack_top)
     mov 0x30(%r14), %r14
     cmp $0x0, EG(exception)
     mov (%r14), %r15
     jnz JIT$$leave_throw
     add $0x20, %r15
     add $0x10, %rsp
     jmp (%r15)
.L4:
     mov $0x45543818, %r15
     jmp JIT$$interrupt_handler

你们能够尝试阅读这段汇编,好比其中针对i的递增,能够看到优化力度很大,好比由于i是局部变量直接分配在寄存器中,i的范围推断不会大于1000000,因此不须要判断是否整数溢出等等。

而若是咱们采用opcache.jit=1005, 如前面的介绍,也就是不使用寄存器分配,能够获得以下结果:

JIT$simple: ; (/tmp/1.php)
     sub $0x10, %rsp
     mov $0x0, 0x50(%r14)
     mov $0x4, 0x58(%r14)
     jmp .L2
.L1:
     add $0x1, 0x50(%r14)
.L2:
     cmp $0x0, EG(vm_interrupt)
     jnz .L4
     cmp $0xf4240, 0x50(%r14)
     jl .L1
     mov 0x10(%r14), %rcx
     test %rcx, %rcx
     jz .L3
     mov $0x1, 0x8(%rcx)
.L3:
     mov 0x30(%r14), %rax
     mov %rax, EG(current_execute_data)
     mov 0x28(%r14), %edi
     test $0x9e0000, %edi
     jnz JIT$$leave_function
     mov %r14, EG(vm_stack_top)
     mov 0x30(%r14), %r14
     cmp $0x0, EG(exception)
     mov (%r14), %r15
     jnz JIT$$leave_throw
     add $0x20, %r15
     add $0x10, %rsp
     jmp (%r15)
.L4:
     mov $0x44cdb818, %r15
     jmp JIT$$interrupt_handler

能够看到针对i的部分,如今是在内存操做,并无使用寄存器。

再若是咱们采用opcache.jit=1201, 咱们能够获得以下结果:

JIT$simple: ; (/tmp/1.php)
     sub $0x10, %rsp
     call ZEND_QM_ASSIGN_NOREF_SPEC_CONST_HANDLER
     add $0x40, %r15
     jmp .L2
.L1:
     call ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED_HANDLER
     cmp $0x0, EG(exception)
     jnz JIT$$exception_handler
.L2:
     cmp $0x0, EG(vm_interrupt)
     jnz JIT$$interrupt_handler
     call ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ_HANDLER
     cmp $0x0, EG(exception)
     jnz JIT$$exception_handler
     cmp $0x452a0858, %r15d
     jnz .L1
     add $0x10, %rsp
     jmp ZEND_RETURN_SPEC_CONST_LABEL

这就只是简单的内敛部分opcode handler的调用了。

你也能够尝试各类opcache.jit的策略结合debug的配置,来观测结果的不一样,也能够尝试各类opcache.jit_debug的配置,好比0xff,将会有更多的辅助信息输出。

相关文章
相关标签/搜索