PHP7新特性之抽象语法树(AST)带来的变化详解


Posted in PHP onJuly 17, 2018

本文分析了PHP7新特性之抽象语法树(AST)带来的变化。分享给大家供大家参考,具体如下:

这里大部分内容参照 AST 的 RFC 文档而成:https://wiki.php.net/rfc/abstractsyntaxtree,为了易于理解从源文档中节选部分进行介绍。

本文并不会告诉你抽象语法树是什么,这需要你自己去了解,这里只是描述 AST 给 PHP 带来的一些变化。

新的执行过程

PHP7 的内核中有一个重要的变化是加入了 AST。在 PHP5中,从 php 脚本到 opcodes 的执行的过程是:

  1. Lexing:词法扫描分析,将源文件转换成 token 流;
  2. Parsing:语法分析,在此阶段生成 op arrays。

PHP7 中在语法分析阶段不再直接生成 op arrays,而是先生成 AST,所以过程多了一步:

  1. Lexing:词法扫描分析,将源文件转换成 token 流;
  2. Parsing:语法分析,从 token 流生成抽象语法树;
  3. Compilation:从抽象语法树生成 op arrays。

执行时间和内存消耗

从以上的步骤来看,这比之前的过程还多了一步,所以按常理来说这反而会增加程序的执行时间和内存的使用。但事实上内存的使用确实增加了,但是执行时间上却有所降低。

以下结果是使用小(代码大约 100 行)、中(大约 700 行)、大(大约 2800 行)三个脚本分别进行测试得到的,测试脚本: https://gist.github.com/nikic/289b0c7538b46c2220bc.

每个文件编译 100 次的执行时间(注意文章的测试结果时间是 14 年,PHP7 还叫 PHP-NG 的时候):

php-ng php-ast diff
SMALL 0.180s 0.160s -12.5%
MEDIUM 1.492s 1.268s -17.7%
LARGE 6.703s 5.736s -16.9%

单次编译中的内存峰值:

php-ng php-ast diff
SMALL 378kB 414kB +9.5%
MEDIUM 507kB 643kB +26.8%
LARGE 1084kB 1857kB +71.3%

单次编译的测试结果可能并不能代表实际使用的情况,以下是使用 PhpParser 进行完整项目测试得到的结果:

php-ng php-ast diff
TIME 25.5ms 22.8ms -11.8%
MEMORY 2360kB 2482kB +5.1%

测试表明,使用 AST 之后程序的执行时间整体上大概有 10% 到 15% 的提升,但是内存消耗也有增加,在大文件单次编译中增加明显,但是在整个项目执行过程中并不是很严重的问题。

还有注意的是以上的结果都是在没有 Opcache 的情况下,生产环境中打开 Opcache 的情况下,内存的消耗增加也不是很大的问题。

语义上的改变

如果仅仅是时间上的优化,似乎也不是使用 AST 的充足理由。其实实现 AST 并不是基于时间优化上的考虑,而是为了解决语法上的问题。下面来看一下语义上的一些变化。

yield 不需要括号

在 PHP5 的实现中,如果在一个表达式上下文(例如在一个赋值表达式的右侧)中使用 yield,你必须在 yield 申明两边使用括号:

<?php
$result = yield fn(); // 不合法的
$result = (yield fn()); // 合法的

这种行为仅仅是因为 PHP5 的实现方式的限制,在 PHP7 中,括号不再是必须的了。所以下面这些写法也都是合法的:

<?php
$result = yield;
$result = yield $v;
$result = yield $k => $v;

当然了,还得遵循 yield 的应用场景才行。

括号不影响行为

在 PHP5 中,($foo)['bar'] = 'baz'$foo['bar'] = 'baz' 两个语句的含义不一样。事实上前一种写法是不合法的,你会得到下面这样的错误:

<?php
($foo)['bar'] = 'baz';
# PHP Parse error: Syntax error, unexpected '[' on line 1

但是在 PHP7 中,两种写法表示同样的意思。

同样,如果函数的参数被括号包裹,类型检查存在问题,在 PHP7 中这个问题也得到了解决:

<?php
function func() {
 return [];
}
function byRef(array &$a) {
}
byRef((func()));

以上代码在 PHP5 中不会告警,除非使用 byRef(func()) 的方式调用,但是在 PHP7 中,不管 func() 两边有没有括号都会产生以下错误:

PHP Strict standards:  Only variables should be passed by reference ...

list() 的变化

list 关键字的行为改变了很多。list 给变量赋值的顺序(等号左右同时的顺序)以前是从右至左,现在是从左到右:

<?php
list($array[], $array[], $array[]) = [1, 2, 3];
var_dump($array);
// PHP5: $array = [3, 2, 1]
// PHP7: $array = [1, 2, 3]
# 注意这里的左右的顺序指的是等号左右同时的顺序,
# list($a, $b) = [1, 2] 这种使用中 $a == 1, $b == 2 是没有疑问的。

产生上面变化的原因正是因为在 PHP5 的赋值过程中,3 会最先被填入数组,1 最后,但是现在顺序改变了。

同样的变化还有:

<?php
$a = [1, 2];
list($a, $b) = $a;
// PHP5: $a = 1, $b = 2
// PHP7: $a = 1, $b = null + "Undefined index 1"

这是因为在以前的赋值过程中 $b 先得到 2,然后 $a 的值才变成 1,但是现在 $a 先变成了 1,不再是数组,所以 $b 就成了 null

list 现在只会访问每个偏移量一次:

<?php
list(list($a, $b)) = $array;
// PHP5:
$b = $array[0][1];
$a = $array[0][0];
// PHP7:
// 会产生一个中间变量,得到 $array[0] 的值
$_tmp = $array[0];
$a = $_tmp[0];
$b = $_tmp[1];

空的 list 成员现在是全部禁止的,以前只是在某些情况下:

<?php
list() = $a;   // 不合法
list($b, list()) = $a; // 不合法
foreach ($a as list()) // 不合法 (PHP5 中也不合法)

引用赋值的顺序

引用赋值的顺序在 PHP5 中是从右到左的,现在时从左到右:

<?php
$obj = new stdClass;
$obj->a = &$obj->b;
$obj->b = 1;
var_dump($obj);
// PHP5:
object(stdClass)#1 (2) {
 ["b"] => &int(1)
 ["a"] => &int(1)
}
// PHP7:
object(stdClass)#1 (2) {
 ["a"] => &int(1)
 ["b"] => &int(1)
}

__clone 方法可以直接调用

现在可以直接使用 $obj->__clone() 的写法去调用 __clone 方法。__clone 是之前唯一一个被禁止直接调用的魔术方法,之前你会得到一个这样的错误:

Fatal error: Cannot call __clone() method on objects - use 'clone $obj' instead in ...

变量语法一致性

AST 也解决了一些语法一致性的问题,这些问题是在另外一个 RFC 中被提出的:https://wiki.php.net/rfc/uniform_variable_syntax.

在新的实现上,以前的一些语法表达的含义和现在有些不同,具体的可以参照下面的表格:

Expression PHP5 PHP7
$$foo['bar']['baz'] ${$foo['bar']['baz']} ($$foo)['bar']['baz']
$foo->$bar['baz'] $foo->{$bar['baz']} ($foo->$bar)['baz']
$foo->$bar['baz']() $foo->{$bar['baz']}() ($foo->$bar)['baz']()
Foo::$bar['baz']() Foo::{$bar['baz']}() (Foo::$bar)['baz']()

整体上还是以前的顺序是从右到左,现在从左到右,同时也遵循括号不影响行为的原则。这些复杂的变量写法是在实际开发中需要注意的。

希望本文所述对大家PHP程序设计有所帮助。

PHP 相关文章推荐
php实现ping
Oct 09 PHP
强烈推荐:php.ini中文版(2)
Oct 09 PHP
php在页面中调用fckeditor编辑器的方法
Jun 10 PHP
浅析PHP编程中10个最常见的错误
Aug 08 PHP
PHP扩展模块memcached长连接使用方法分析
Dec 24 PHP
php采集中国代理服务器网的方法
Jun 16 PHP
详解PHP中的状态模式编程
Aug 11 PHP
详解php中的implements 使用
Jun 13 PHP
yii2中LinkPager增加总页数和总记录数的实例
Aug 28 PHP
PHP7匿名类的用法示例
Apr 05 PHP
Laravel5.7 Eloquent ORM快速入门详解
Apr 12 PHP
php获取目录下所有文件及目录(多种方法)(推荐)
May 14 PHP
阿里云的WindowsServer2016上部署php+apache
Jul 17 #PHP
tp5实现微信小程序多图片上传到服务器功能
Jul 16 #PHP
PHP 爬取网页的主要方法
Jul 13 #PHP
php实现微信发红包功能
Jul 13 #PHP
Yii2框架redis基本应用示例
Jul 13 #PHP
Yii2框架实现登陆添加验证码功能示例
Jul 12 #PHP
Yii框架日志记录Logging操作示例
Jul 12 #PHP
You might like
基于mysql的论坛(2)
2006/10/09 PHP
ThinkPHP标签制作教程
2014/07/10 PHP
symfony表单与页面实现技巧
2015/01/26 PHP
基于Laravel实现的用户动态模块开发
2017/09/21 PHP
PHP PDOStatement::rowCount讲解
2019/02/01 PHP
List the UTC Time on a Computer
2007/06/11 Javascript
jquery实现图片渐变切换兼容ie6/Chrome/Firefox
2013/08/02 Javascript
JS针对浏览器窗口关闭事件的监听方法集锦
2016/06/24 Javascript
WebStorm ES6 语法支持设置&amp;babel使用及自动编译(详解)
2017/09/08 Javascript
nodejs 日志模块winston的使用方法
2018/05/02 NodeJs
解决Mac node版本升级失败的问题
2018/05/16 Javascript
通过jquery的ajax请求本地的json文件方法
2018/08/08 jQuery
vue router动态路由设置参数可选问题
2019/08/21 Javascript
[45:59]完美世界DOTA2联赛PWL S2 FTD vs GXR 第二场 11.22
2020/11/24 DOTA
基于Python实现的扫雷游戏实例代码
2014/08/01 Python
python实现通过代理服务器访问远程url的方法
2015/04/29 Python
Python实现二分查找与bisect模块详解
2017/01/13 Python
用Python登录好友QQ空间点赞的示例代码
2017/11/04 Python
centos 安装python3.6环境并配置虚拟环境的详细教程
2018/02/22 Python
Python访问MongoDB,并且转换成Dataframe的方法
2018/10/15 Python
分享Python切分字符串的一个不错方法
2018/12/14 Python
Python如何操作office实现自动化及win32com.client的运用
2020/04/01 Python
利用CSS3实现文本框的清除按钮相关的一些效果
2015/06/23 HTML / CSS
教你使用Canvas处理图片的方法
2017/11/28 HTML / CSS
Born鞋子官网:Born Shoes
2017/04/06 全球购物
美国最大的无人机经销商:DroneNerds
2018/03/20 全球购物
美国巧克力喷泉品牌:Sephra
2019/05/05 全球购物
如何执行一个shell程序
2012/11/23 面试题
财务会计人员求职的自我评价
2014/01/13 职场文书
新浪微博实习心得体会
2014/01/27 职场文书
三年级数学教学反思
2014/01/31 职场文书
蓝颜请假条
2014/04/11 职场文书
小学生手册家长评语
2014/04/16 职场文书
整顿机关作风心得体会
2014/09/10 职场文书
公司租房协议书
2014/10/14 职场文书
幼儿教师远程研修感悟
2015/11/18 职场文书