PHP 8新特性之JIT

PHP8 alpha1已经在6月25日发布,其中包含了不少的新特性,当然我自己认为最重要的还是JIT,相信关于JIT也是大家最关心的,它到底怎么用,有什么要注意的,以及性能提升到底咋样?今天我们就来扒扒JIT

JIT :Just In Time, 一种编译器策略,将代码在运行时转换为依赖于体系结构的机器码,并即时执行。

JIT 的好处:

1. 目前已经很难通过常规手段提升 PHP 的性能,JIT 基本上是目前性能提升的唯一手段;

2. JIT 带来的性能提升可以让 PHP 在更多使用场景( CPU 密集)中发挥作用;

3. 可以使用 PHP 来开发内置函数,而不用担心性能方面的问题。这一方面可以加速语言的发展(更多人可以参与进来),同时也可以减少目前使用 C 开发容易出现的内存管理、溢出等问题

主要是 JIT 带来的性能提升,以及性能提升对整个语言使用场景的扩展及语言生态发展的支持。

首先,我们来看一张图:

左图是PHP8之前的Opcache流程示意图,右图是PHP8中的Opcache示意图,可以裁剪几个关键点:

1. Opcache会做opcode层面的优化,某些图中的俩条opcode合并为一条

2. PHP8的JIT目前是在Opcache之中提供的

3. JIT在Opcache优化之后的基础上,结合Runtime的信息再次优化,直接生成机器码

4. JIT不是原来的Opcache优化的替代,是增强

5. 目前PHP8只支持x86架构的CPU

实际上JIT共享了很多原来的Opcache做优化的基础数据结构,比如data flow graph, call graph, SSA等,关于这部分,后续如果有时间,可以单独在写一个文章来介绍,今天只是只是着重在使用焦点。

下载安装好以后,除掉初始的opcache配置以外,对于JIT我们需要添加如下配置到php.ini:

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

根据此,我们可以大概得到如下几个附图:

尽量使用12x5型的配置,此时应该是效果最优的

对于x,如果是脚本等级的,推荐使用0,如果是Web服务型的,可以根据测试结果选择3或5

@jit的形式,在有了属性以后,可能变成<< 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

这就只是简单的内敛部分操作码处理程序的调用了。

您也可以尝试各种opcache.jit的策略绑定调试的配置,来观察结果的不同,也可以尝试各种opcache.jit_debug的配置,大约0xff,将会有更多的辅助信息输出。

以上内容希望帮助到大家,很多PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货需要的可以免费分享给大家,需要的可以加入我的官方群953224940。

进阶PHP月薪30k>>>架构师成长路线【视频、面试文档免费获取】

你可能感兴趣的:(PHP 8新特性之JIT)