我们常用的高级语言有很多种,比较出名的有C\C++、Python、PHP、Go、Pascal等。而这些语言根据运行的方式不同,大体分为两种:
编译型语言和解释型语言
。
其中,编译型语言包括C\C++、Pascal、Go等。这里说的编译是指在应用源程序执行之前
,就将程序源代码编译成汇编语言
,然后进一步根据软硬件环境"翻译"成目标文件
。一般称完成编译工作的工具为编译器
。而解释型语言,在程序运行时才被“翻译”为机器语言
。但是执行一次“翻译”一次
,所以执行效率较低。解释器
的工作就是解释型语言中,负责“翻译”源代码的程序
。
我们对编译型语言与解释型语言的区别的理解
,立足于源代码被编译成目标平台CPU指令的时机
。对于编译型语言,编译结果已经是针对当前CPU体系的指令
;而解释型语言,需要先编译成中间代码,再经由该解释型语言的特定虚拟机,翻译成特定CPU体系的指令被执行
。解释型语言是在运行过程中,翻译为目标平台的指令
。常说解释型语言“慢”,主要也是慢在这里
。
php7中执行原理
在PHP 7中,
源代码
首先进行词法分析
,将源代码切割为多个字符串单元
,分割后的字符串
称为Token
。而一个一个独立的Token
是无法表达完整语义
的,需经过语法分析
阶段,将Token转换为抽象语法树
(简称AST)。之后,抽象语法树被转换为机器指令执行
。在PHP中,这些指令称为opcode
(以后会对opcode做更详细的解释,此处可以将其看待为CPU指令)。
到AST的生成这一步,编译型语言与解释型语言所需经历的过程相似
。从抽象语法树之后开始产生差异。
简图(最后一步的左侧分支是编译型语言的过程)
第1步:源码通过词法分析得到Token。
第2步:基于语法分析器生成抽象语法树(AST)。
第3步:抽象语法树转换为opcodes(opcode指令集合),PHP解释执行opcodes。
接下来在基本步骤的基础上,细化PHP语言的执行原理,以便更清晰地建立认知。
第1步:词法分析将PHP代码转换为
有意义的标识Token
。该步骤的词法分析器使用Re2c
实现。
第2步:语法分析将
Token和符合文法规则的代码
生成抽象语法树
。语法分析器基于Bison实现
。语法分析使用了BNF
(Backus-Naur Form,巴科斯范式)来表达文法规则
,Bison借助状态机
、状态转移表
和压栈、出栈
等一系列操作,生成抽象语法树。
第3步:上步的
抽象语法树生成对应的opcode
,并被虚拟机执行。opcode
是PHP 7定义的一组指令标识,指令对应着相应的handler(处理函数
)。当虚拟机调用opcode,会找到opcode背后的处理函数,执行真正的处理
。以常见的echo语句为例,其对应的opcode便是ZEND_ECHO。
注意 这里为了便于理解词法分析和语法分析过程,将两者分开描述。但实际情况下,出于效率考虑,两个过程
并非完全独立
。
下面通过一段示例代码,来建立PHP 7运转的初步理解。
echo "hello world";
这段代码首先会被切割为Token
Token
Token是PHP代码
被切割成的有意义的标识
。本次使用的PHP 7版本中有137种Token
,在zend_language_parser.h文件
中做了定义
http://php.net/manual/zh/tokens.php
PHP提供了token_get_all()函数
来获取PHP代码被切割后的Token
,可以在深入源码学习前,粗略查看PHP代码被切割后的Token。对于如下代码片段:
./php -r 'print_r(token_get_all("<?php echo \"hello world\";?>"));'
//输出:
Array
(
[0] => Array
(
[0] => 379
[1] => <?php
[2] => 1
)
[1] => Array
(
[0] => 328
[1] => echo
[2] => 1
)
[2] => Array
(
[0] => 382
[1] =>
[2] => 1
)
[3] => Array
(
[0] => 323
[1] => "hello world"
[2] => 1
)
[4] => ;
[5] => Array
(
[0] => 381
[1] => ?>
[2] => 1
)
)
其中,二维数组的每个成员数组的
第一个值为Token对应的枚举值
。第二个值为Token对应的原始字符串内容
。第三个值为代码对应的行号
。可以看出,词法解析器将“<?php echo"hello world";”这段文本内容切分成了4部分。
1)文本“<?php”,切割后对应的Token值为379,参考PHP 7中的源码:
2)echo对应的Token是T_ECHO,对应的Token值为328:
3)源码中的空格,对应的Token为T_WHITESPACE,值为382:
4)字符串"hello world",对应的Token值为323:
可见,Token就是一个个的“词块”,但是单独存在的词块
不能表达完整的语义
,还需要借助规则进行组织串联。语法分析器就是这个组织者。它会检查语法,匹配Token,对Token进行关联
。
PHP 7中,组织串联
的产物
就是AST
(Abstract Syntax Tree,抽象语法树)。
AST
AST是PHP 7版本新特性
。在这之前的版本中,PHP代码的执行过程中是没有生成AST这一步的。PHP 7对抽象语法树的支持,实现了PHP编译器和解释器解耦,有效提升了可维护性
。
顾名思义,抽象语法树具有树状结构
。AST的节点
分为多种类型
,对应着PHP语法
。我们可以认为节点类型
是对语法规则的抽象
,例如赋值语句,生成的抽象语法树节点为ZEND_AST_ASSIGN。而赋值语句的左右操作数又将作为ZEND_AST_ASSIGN类型节点的孩子。通过这样的节点关系,构建出抽象语法树。
PHP-Parser工具,它可以用来查看PHP代码生成的AST。
注意 PHP-Parser是PHP 7内核作者之一Nikic编写的将PHP源码生成AST的工具。源码见https://github.com/nikic/PHP-Parser。
opcodes
AST扮演了源码到中间代码的
临时存储介质
的角色,还需要将其转换为opcode
,才能被引擎直接执行
。opcode只是单条指令,opcodes是opcode的集合形式
,是PHP执行过程中的中间代码
,类似Java中的字节码。opcode生成之后由虚拟机执行。
我们知道,PHP工程优化措施
中有一个比较常见的“开启opcache
”,指的就是这里的opcodes的缓存
(opcodes cache)。通过省去从源码到opcode的阶段
,引擎可以直接执行缓存的opcode,以此提升性能
。
借助vld插件,可以直观地看到一段PHP代码生成的opcode:(未完待续)