一直学一直嗨,一直嗨一直学

世界上最好的编程语言PHP

万众期待的世界上最好的编程语言——PHP,最新版PHP8在2020-11-26正式版发布了。

它包含了很多新功能与优化项, 包括命名参数、联合类型、注解、构造器属性提升、match 表达式、nullsafe 运算符、JIT,并改进了类型系统、错误处理、语法一致性。

今天来具体分析些这些主要的新的更新点:

0. JIT编译 (Just In Time Compilation)
1. 不开启Opcache
2. 开启Opcacheh后的执行过程
3. 开启JIT后的执行过程
4. JIT的配置。
5. 性能对比
2. 命名参数(Named arguments)
3. 联合类型(Union Types)
4. match表达式(Match Expressions)
5. 构造器属性提升( constructor property promotion)
5. 注解(Attributes)
6. Nullsafe 运算符 (Nullsafe Operator)
7. 更合理的字符串与数字比较

 

JIT编译 (Just In Time Compilation)

JIT算是PHP最值得期待的一个功能了,JIT编译技术,它通过将OpCodes编译为机器码,进一步提高了代码的执行速度。对于 CPU 密集型计算,性能有显著的提升。

下面这张图是一个对比,在开启了JIT后的性能对比,性能的提升可以说是非常的明显的。

 

世界上最好的编程语言PHP

上面说到,JIT是基于OpCodes的,那么就简单的分析下PHP执行的原理

 

1. 不开启Opcache

在不开启Opcache时,PHP代码执行的步骤:
世界上最好的编程语言PHP

执行PHP代码,分别是以下4步:

  1. Lexing,将PHP代码转换为语言片段(Tokens)
  2. Parsing, 将Tokens转换成简单而有意义的抽象语法树表达式
  3. Compilation,将表达式编译成Opcodes
  4. Execution,Zend引擎顺次执行Opcodes

. 开启Opcacheh后的执行过程

世界上最好的编程语言PHP

Opcache开启后,它通过添加一个内存共享缓存层,将编译后的Opcodes缓存起来,下次执行相同的代码的时候,直接取出给到zend VM引擎去执行。

这样就省去了 Lexing 、 Parsing 和 Compiling 这3个步骤,加快了程序的执行速度。

3. 开启JIT后的执行过程

世界上最好的编程语言PHP

开启JIT支持后,JIT在Opcache优化之后的基础上,结合Runtime的信息再次优化,直接生成机器码并进行缓存。下次执行相同的代码的时候,将直接从缓存中取出机器码进行执行。

这样,把Zend VM引擎去执行Opcodes生成机器码这一步也给省去了。

4. JIT的配置。

JIT是在opcache的基础之上的,所以得先在php.ini中开启opcode:

zend_extension = opcache.so 
opcache.enable = 1 
opcache.enable_cli = 1

然后就是JIT自己的配置了:

opcache.jit=1205
opcache.jit_buffer_size=64M

opcache.jit 1205 这4个数字由4个配置项组成

1,是否在生成机器码点时候使用AVX指令, 需要CPU支持: (0,1)
2,寄存器分配策略:(0~2)
3,JIT触发策略:(0~5)
4,JIT优化策略,数值越大优化力度越大: (0~5)

其中这4项的具体值说明如下:

是否在生成机器码点时候使用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

看上去比较复杂,总结一下就是:

尽量使用12×5型的配置,此时应该是效果最优的。对于x, 如果是脚本级别的,推荐使用0,如果是Web服务型的,可以根据测试结果选择3或5

5. 性能对比

我们分别在php7和php8下面,去执行相同的测试代码,看下性能对比情况

PHP7:

$ php -d opcache.jit_buffer_size=0 Zend/bench.php

simple             0.006
simplecall         0.003
simpleucall        0.003
simpleudcall       0.003
mandel             0.023
mandel2            0.036
ackermann(7)       0.012
ary(50000)         0.003
ary2(50000)        0.002
ary3(2000)         0.029
fibo(30)           0.045
hash1(50000)       0.006
hash2(500)         0.007
heapsort(20000)    0.017
matrix(20)         0.015
nestedloop(12)     0.014
sieve(30)          0.008
strcat(200000)     0.003
------------------------
Total              0.236

 

PHP8:

php -d opcache.jit_buffer_size=64M -d opcache.jit=1205 Zend/bench.php

simple 0.002
simple             0.001
simplecall         0.000
simpleucall        0.001
simpleudcall       0.000
mandel             0.005
mandel2            0.006
ackermann(7)       0.006
ary(50000)         0.002
ary2(50000)        0.002
ary3(2000)         0.010
fibo(30)           0.019
hash1(50000)       0.004
hash2(500)         0.003
heapsort(20000)    0.008
matrix(20)         0.008
nestedloop(12)     0.005
sieve(30)          0.002
strcat(200000)     0.002
------------------------
Total              0.085

 

对比下来:对于Zend/bench.php, 开启JIT了以后,耗时降低将近(70%),性能提升将近(2倍)。

2. 命名参数(Named arguments)

当我们创建一个函数时,例如:

function Get($name, $age, $sex) 
{
//
}

 

在调用这个函数时,我们需要顺序输入参数:

Get("jack", 18, 1)

 

但在PHP8中,我们可以这样做:

Get(name: "jack", age: 18, sex: 1)

 

传入参数的同时,指定这个参数的名字。这样的好处就是我们可以改变顺序进行传递了:

Get(name: "jack", age: 18, sex: 1)
Get(age: 18, name: "jack",sex: 1)
Get(sex: 1, age: 18, name: "jack")

 

对于那些有默认值的参数,我们就可以跳过这个值,通过参数来传递值

function Create($name, $sex = 1, $age) 
{
//
}

Create(name: "jack", age: 18)

当然也可以新旧方式一起使用:

Create("jack", age: 18)

 

这个新功能,虽然看起来有点怪怪的,但是好像也没什么卵用 -_-||

3. 联合类型(Union Types)

在PHP7中,当我们开启了强制标识后,对于函数的参数变量类型和返回值类型,都是唯一固定的,比如:

function Get(string $name, int $age, int $sex): bool {}

 

但是这又和PHP是一个弱类型语言相违背,整体的灵活性大打折扣了。在PHP8中,我们就可以使用联合类型,来什么多种类型:

function Get(string|int $name, int $age, int $sex): bool|string {}

 

这样一来,既可以限制强制类型,又扩展了多种类型。

来看下官方的一个例子:

class Number {
    private int|float $number;
 
    public function setNumber(int|float $number): void {
        $this->number = $number;
    }
 
    public function getNumber(): int|float {
        return $this->number;
    }
}

4. match表达式(Match Expressions)

PHP8语法糖中,新增1个语法表达式:match,它和switch case有一些相同之处,但是又多了很多的方便,我们分别对比下:

switch ($input) {
    case "true":
        $result = 1;
    break;
    case "false":
        $result = 0;
    break;
    case "null":
        $result = NULL;
    break;
}

在PHP8种,你可以用match表达式来写,更加简单和方便:

$result = match($input) {
        "true" => 1,
        "false" => 0,
        "null" => NULL,
};

azing啊,我们知道,switch匹配1个case后,是需要加break关键字的,相信大家都多多少少踩过这个坑。现在有了match关键字,就可以轻松躲过这个坑,而且它会直接返回值,可以直接赋值给$result了。

同样,类似switch的多个case一个block一样,match的多个条件也可以写在一起,比如:

$result = match($input) {
    "true", "on" => 1,
    "false", "off" => 0,
    "null", "empty", "NaN" => NULL,
};

 

需要注意的和switch不太一样的是,以前我们用switch可能会经常遇到这种诡异的问题:

$input = "2 person";
switch ($input) {
    case 2:
        echo "bad";
    break;
}

// 输出: bad

你会发现,bad竟然被输出了,这是因为switch使用了宽松比较==。match就不会有这个问题了, 它使用的是严格比较===,就是值和类型都要完全相等。

还有就是,当input并不能被match中的所有条件满足的时候,match会抛出一个UnhandledMatchError exception。

5. 构造器属性提升( constructor property promotion)

这个听起来有点拗口,简而言之就是:我们可以在类的构造函数__construct中来申明并赋值类属性了,而不必提前定义。

在PHP8之前,我们一般会这样定义一个类,首先要设置成员变量,然后在构造或者某一个方法为它赋值。比如:

class Point {
  public float $x;
  public float $y;
  public float $z;

  public function __construct(float $x = 0.0,float $y = 0.0,float $z = 0.0) {
    $this->x = $x;
    $this->y = $y;
    $this->z = $z;
  }
}

 

PHP8种我们可以这样写,将定义放入到括号参数中:

class Point {
  public function __construct(
    public float $x = 0.0,
    public float $y = 0.0,
    public float $z = 0.0,
  ) {}
}

 

这种写法虽然有点骚气,但是,确实省略了一些代码量。

5. 注解(Attributes)

注解算是PHP8种的1个大的更新。新增的注解让原先的注释有了可编程的能力,我们就可以利用注解来完成一些配置化和自动文档的东西。

先来看下php8中1个注解的例子:

#[Params("Foo", "argument")]
#[See("https://xxxxxxxx/xxxx/xxx.html")]
#[Route("/api/posts/{id}", methods: ["GET"], return: "bool")]
function dummy($argument) {}

 

这个语法看起来有点奇怪,和之前常见的注释@param有点区别的

#[Name(Arguments)]
#[Name(Argunment1, Arguments2, ArgumentN)]
 
#[Name1(Argument), Name2(Argument), Name3(Argument)]

 

我们可以用,反射的方式,来获取注解的内容:

$ref = new ReflectionFunction("dummy");

var_dump($ref->getAttributes("Params")[0]->getName());
var_dump($ref->getAttributes("Params")[0]->getArguments());

var_dump($ref->getAttributes("See")[0]->getName());
var_dump($ref->getAttributes("See")[0]->getArguments());

var_dump($ref->getAttributes("Route")[0]->getArguments());

 

看下打印结果:

string(6) "Params"
array(2) {
  [0]=>
  string(3) "Foo"
  [1]=>
  string(8) "argument"
}

string(3) "See"
array(1) {
  [0]=>
  string(30) "https://xxxxxxxx/xxxx/xxx.html"
}

array(3) {
  [0]=>
  string(15) "/api/posts/{id}"
  ["methods"]=>
  array(1) {
    [0]=>
    string(3) "GET"
  }
  ["return"]=>
  string(4) "bool"
}

因为注解采用的是#注释的方式所以,在非php8的环境中,也不会出现语法错误。

6. Nullsafe 运算符 (Nullsafe Operator)

Nullsafe 运算符的写法为:?->

现在可以用新的 nullsafe 运算符链式调用,而不需要条件检查 null。 如果链条中的一个元素失败了,整个链条会中止并认定为 Null。

看一个列子,我们在php7中是这样使用的:

$country =  null;
if ($session !== null) {
  $user = $session->user;
  if ($user !== null) {
    $address = $user->getAddress();
 
    if ($address !== null) {
      $country = $address->country;
    }
  }
}

 

各种if else 看的人眼花缭乱。在php8中,我们就可以用 Nullsafe 改进试一试:

$country = $session?->user?->getAddress()?->country;

7. 更合理的字符串与数字比较

在 PHP8 比较数字字符串(numeric string)时,会按数字进行比较。 不是数字字符串时,将数字转化为字符串,按字符串比较。

//PHP7
0 == 'foobar' // true

//PHP8
0 == 'foobar' // false

 

更多其他新的优化和更新点,请见官方文档说明:https://www.php.net/releases/8.0/zh.php?lang=zh