PHP 연산 코드 최적화의 깊이 이해

 

1. 개요

구문이어서 워드 심볼로 변환 할 수있는 어휘 분석기를 통해 읽기, 스크립트 문자열 : PHP 젠드 가상 머신의 구현 과정에 동적 스크립트 언어로 (PHP 버전 본원 케이스 7.1.3 설명된다) 문법 구조 후 발견 추상 구문 트리를 생성하는 분석기는 다음 컴파일러는 정적 오피 코드를 생성하고, 마지막으로 각 연산 코드의 실행에 아날로그 기계 명령어를 해석했다.

전체 체인, 오피 컴파일러 최적화 기술이 데드 코드 제거 일정한 전파 조건, 기능 인라인을 간소화하기 위해 다양한 최적화 등 오피 코드 실행의 성능을 향상시킬 목적으로, 적용될 수있다 생성.

PHP 확장 opcache, 공유 메모리 캐시 최적화 지원을 기반으로 생성 된 오피합니다. 이를 바탕으로, 그것은 정적으로 컴파일 된 연산 코드 최적화에 합류했다. 최적화는 일반적으로 각각의 최적화를 설명하기 위해, 변환 원리 최적화 패스의 일반적인 용도 (최적화 패스)를 관리하는 것으로 최적화 (최적화)에 사용된다.

전반적으로, 최적화는 두 가지 방법으로 전달합니다

  • 정보 패스 변환 분석은 원 패스 보조 정보에 대한 데이터 흐름 제어 흐름 분석을 제공하는 단계;
  • 하나는 보통 전에, 삭제 명령, 변경 지시 또는, 조정 명령 시퀀스를 포함하여, 코드 생성을 변경하며, 각 패스를 생성 코드 변경 덤프 있고, 패스 전환된다.

출발점으로 PHP 원단위 op_array 컴파일 제공된 최적화 opcache 확장, PHP 동작 코드 실행의 최소 단위와 결합 컴파일러 이론에 근거. 젠드 가상 머신, 다양한 최적화를 빗질하는 것은 전달에 컴파일러 최적화 기술을 설명하면 성능을 향상시키기 위해 코드 실행 연산 코드를 최적화하는 방법을 단계별로 단계입니다. 마지막으로, PHP 언어 가상 머신의 실행은 약간의 기대를 주어진.

2. 몇 가지 개념을 설명

1) 정적으로 컴파일 / 해석 / 시간 편집

정적 또한 미리 컴파일 (AOT 컴파일)라고도 (정적 컴파일) 컴파일은 AOT라고 함. 즉, 오브젝트 코드로 소스 코드를 컴파일 오브젝트 코드가 실행될 지원하는 플랫폼에서 실행되고 있습니다.

정적 컴파일러에 대해 동적 컴파일 (동적 컴파일)는, "실행시에 컴파일."를 의미 일반적으로 고용 인터프리터 (통역) 컴파일러 구현, 그것은 소스 언어의 해석 일을 의미한다.

JIT 컴파일러 (JIT 컴파일), 즉 시간 컴파일, 좁은 의미의 코드 조각을 말한다는 시간이 처음으로 실행하는 것입니다 컴파일, 당신은 직접 실행을 컴파일 할 필요가 없습니다, 그것은 동적 컴파일의 특별한 경우이다.

아래와 같이 다른 컴파일러의 세 가지 유형은 일반적으로 기술 될 수 있으며, 프로세스를 수행고도

2) 데이터 흐름 / 제어 흐름

컴파일러 최적화는 모든 컴파일러 최적화의 기초 프로그램에서 충분한 정보를 얻을 필요가있다.

구문 트리의 컴파일러 프론트 엔드에 의해 생성 된 결과는 약간 낮은 중간 코드 될 수있다. 그러나 상관없이 결과가 무엇을해야 하는지를 프로그램을 어떤 형태, 여전히 그것을 수행하는 방법에 많은 정보를 제공하지 않습니다. 각 프로세스 태스크 데이터 흐름 분석 왼쪽 처리 글로벌 정보와 관련된 데이터를 결정하는 것, 흐름 분석을 제어하기 위해 왼쪽에서 컴파일러는 작업 흐름 제어 계층을 발견 할 것이다.

  • 제어 흐름 분석은 종속성의 분석에 기초하여, 데이터 흐름 분석을 위해, 정규 구성 정보 취득 프로그램을 제어한다. 제어의 기본 모델은 제어 흐름 그래프 (제어 흐름 그래프 CFG)이다. 제어 흐름 분석은 단일 공정 사이클의 사용은 노드 간격 분석 두 가지 방법을 찾을 필요했다입니다.

  • 데이터는 프로그램 코드에서 시맨틱 정보의 수집 프로그램 흐름 및 변수의 정의에서 사용 대수적 컴파일. 기본 모델 데이터 (데이터 플로우 그래프, DFG)의 데이터 흐름도이다. 정상 데이터 흐름 분석은 분석 트리 컨트롤 (관리 트리 기반 데이터 흐름 분석)에 기초하여, 분석 구간 분석 알고리즘은 두 구조로 나누어진다.

3) op_array

즉 기본 유닛 C 언어 스택 프레임 유사 (적층 구조)의 개념은, (a) 프로그램을 실행하는 기본 단위는 일반적으로 함수 호출이다. 여기에, PHP 코드를 나타 평가 후면에 전달하는 기능이나 방법, 전체 PHP 스크립트 파일, 문자열 op_array로 컴파일됩니다.

장치의 기본적인 동작을 달성 Op_array 것은 물론 프로그램의 모든 정보는, 가장 중요한 분야의 구조에 대한 연산 코드의 배열을 포함하는 구조이지만, 이외에도 가변 타입의 주석, 이상 정보를 포착 정보 점프를 포함한다.

4) 오피

최소 최적 오피 원단위 op_array 내에서 수행되는 통역자 (ZendVM) 처리에 의해 실행되고, 순차적으로 마지막 RETRUN 복귀 이탈이 특정한 연산 코드까지 현재 오피 코드의 실행은 다음의 연산 코드를 프리 페치 할 실행 이송.

这里的opcode某种程度也类似于静态编译器里的中间表示(类似于LLVM IR),通常也采用三地址码的形式,即包含一个操作符,两个操作数及一个运算结果。其中两个操作数均包含类型信息。此处类型信息有五种,分别为:

  • 编译变量(Compiled Variable,简称CV),编译时变量即为php脚本中定义的变量。
  • 内部可重用变量(VAR),供ZendVM使用的临时变量,可与其它opcode共用。
  • 内部不可重用变量(TMP_VAR),供ZendVM使用的临时变量,不可与其它opcode共用。
  • 常量(CONST),只读常量,值不可被更改。
  • 无用变量(UNUSED)。由于opcode采用三地址码,不是每一个opcode均有操作数字段,缺省时用该变量补齐字段。

类型信息与操作符一起,供执行器匹配选择特定已编译好的C函数库模板,模拟生成机器指令来执行。

opcode在ZendVM中以zend_op结构体来表征,其主体结构如下: 
고도

3.opcache optimizer优化器

PHP脚本经过词法分析、语法分析生成抽象语法树结构后,再经静态编译生成opcode。它作为向不同的虚拟机执行指令的公共平台,依赖不同的虚拟机具体实现(然对于PHP来说,大部分是指ZendVM)。

在虚拟机执行opcode之前,如果对opcode进行优化可得到执行效率更高的代码,pass的作用就是优化opcode,它作用于opcde、处理opcode、分析opcode、寻找优化的机会并修改opcode产生更高执行效率的代码。

1)ZendVM优化器简介

在Zend虚拟机(ZendVM)中,opcache的静态代码优化器即为zend opcode optimization。

为观察优化效果及便于调试,它也提供了优化与调试选项:

  • optimizationlevel (opcache.optimizationlevel=0xFFFFFFFF) 优化级别,缺省打开大部分优化遍,用户亦通过传入命令行参数控制关闭
  • optdebuglevel (opcache.optdebuglevel=-1) 调试级别,缺省不打开,但提供了各优化前后opcode的变换过程

执行静态优化所需的脚本上下文信息则封装在结构zend_script中,如下:

typedef struct _zend_script {  
    zend_string   *filename;        //文件名     zend_op_array main_op_array; //栈帧     HashTable function_table; //函数单位符号表信息     HashTable class_table; //类单位符号表信息 } zend_script; 
123456

上述三个内容信息即作为输入参数传递给优化器供其分析优化。当然与通常的PHP扩展类似,它与opcode缓存模块一起(zend_accel)构成了opcache扩展。其在缓存加速器内嵌入了三个内部API:

  • zendoptimizerstartup 启动优化器
  • zendoptimizescript 优化器实现优化的主逻辑
  • zendoptimizershutdown 优化器产生的资源清理

关于opcode缓存,也是opcode非常重要的优化。其基本应用原理是大体如下:

虽然PHP作为动态脚本语言,它并不会直接调用GCC/LLVM这样的整套编译器工具链,也不会调用Javac这样的纯前端编译器。但每次请求执行PHP脚本时,都经历过词法、语法、编译为opcode、VM执行的完整生命周期。

除去执行外的前三个步骤基本就是一个前端编译器的完整过程,然而这个编译过程并不会快。假如反复执行相同的脚本,前三个步骤编译耗时将严重制约运行效率,而每次编译生成的opcode则没有变化。因此可在第一次编译时把opcode缓存到某一个地方,opcache扩展即是将其缓存到共享内存(Java则是保存到文件中),下次执行相同脚本时直接从共享内存中获取opcode,从而省去编译时间。

opcache扩展的opcode 缓存流程大致如下: 
고도由于本文主要集中讨论静态优化遍,关于缓存优化的具体实现此处不展开。

2)ZendVM优化器原理

依“鲸书”(《高级编译器设计与实现》)所述,一个优化编译器较为合理的优化遍顺序如下:고도
上图中涉及的优化从简单的常量、死代码到循环、分支跳转,从函数调用到过程间优化,从预取、缓存到软流水、寄存器分配,当然也包含数据流、控制流分析。

当然,当前opcode优化器并没有实现上述所有优化遍,而且也没有必要实现机器相关的低层中间表示优化如寄存器分配。

opcache优化器接收到上述脚本参数信息后,找到最小编译单位。以此为基础,根据优化pass宏及其对应的优化级别宏,即可实现对某一个pass的注册控制。

注册的优化中,按一定顺序组织串联各优化,包含常量优化、冗余nop删除、函数调用优化的转换pass,及数据流分析、控制流分析、调用关系分析等分析pass。

zendoptimizescript及实际的优化注册zend_optimize流程如下:

zend_optimize_script(zend_script *script,  
      zend_long optimization_level, zend_long debug_level)
    |zend_optimize_op_array(&script->main_op_array, &ctx);
        遍历二元操作符的常量操作数,由运行时转化为编译时(反向pass2)
        实际优化pass,zend_optimize
        遍历二元操作符的常量操作数,由编译时转化为运行时(pass2)
    |遍历op_array内函数zend_optimize_op_array(op_array, &ctx); |遍历类内非用户函数zend_optimize_op_array(op_array, &ctx); (用户函数设static_variables) |若使用DFA pass & 调用图pass & 构建调用图成功 遍历二元操作符的常量操作数,由运行时转化为编译时(反向pass2) 设置函数返回值信息,供SSA数据流分析使用 遍历调用图的op_array,做DFA分析zend_dfa_analyze_op_array 遍历调用图的op_array,做DFA优化zend_dfa_optimize_op_array 若开调试,遍历dump调用图的每一个op_array(优化变换后) 若开栈矫正优化,矫正栈大小adjust_fcall_stack_size_graph 再次遍历调用图内的的所有op_array, 针对DFA pass变换后新产生的常量场景,常量优化pass2再跑一遍 调用图op_array资源清理 |若开栈矫正优化 矫正栈大小main_op_array 遍历矫正栈大小op_array |清理资源 
1234567891011121314151617181920212223

该部分主要调用了SSA/DFA/CFG这几类用于opcode分析pass,涉及的pass有BB块、CFG、DFA(CFG、DOMINATORS、LIVENESS、PHI-NODE、SSA)。

用于opcode转换的pass则集中在函数zend_optimize内,如下:

zend_optimize  
|op_array类型为ZEND_EVAL_CODE,不做优化
|开debug,    可dump优化前内容
|优化pass1,  常量替换、编译时常量操作变换、简单操作转换
|优化pass2    常量操作转换、条件跳转指令优化
|优化pass3    跳转指令优化、自增转换
|优化pass4    函数调用优化(主要为函数调用优化)
|优化pass5    控制流图(CFG)优化
 |构建流图
 |计算数据依赖
 |划分BB块(basic block,简称BB,数据流分析基本单位)
 |BB块内基于数据流分析优化
 |BB块间跳转优化 |不可到达BB块删除 |BB块合并 |BB块外变量检查 |重新构建优化后的op_array(基于CFG) |析构CFG |优化pass6/7 数据流分析优化 |数据流分析(基于静态单赋值SSA) |构建SSA |构建CFG 需要找到对应BB块序号、管理BB块数组、计算BB块后继BB、标记可到达BB块、计算BB块前驱BB |计算Dominator树 |标识循环是否可简化(主要依赖于循环回边) |基于phi节点构建完SSA def集、phi节点位置、SSA构造重命名 |计算use-def链 |寻找不当依赖、后继、类型及值范围值推断 |数据流优化 基于SSA信息,一系列BB块内opcode优化 |析构SSA |优化pass9 临时变量优化 |优化pass10 冗余nop指令删除 |优化pass11 压缩常量表优化 
1234567891011121314151617181920212223242526272829303132

还有其他一些优化遍如下:

优化pass12   矫正栈大小
优化pass15   收集常量信息
优化pass16   函数调用优化,主要是函数内联优化
123

除此之外,pass 8/13/14可能为预留pass id。由此可看出当前提供给用户选项控制的opcode转换pass有13个。但是这并不计入其依赖的数据流/控制流的分析pass。

3)函数内联pass的实现

通常在函数调用过程中,由于需要进行不同栈帧间切换,因此会有开辟栈空间、保存返回地址、跳转、返回到调用函数、返回值、回收栈空间等一系列函数调用开销。因此对于函数体适当大小情况下,把整个函数体嵌入到调用者(Caller)内部,从而不实际调用被调用者(Callee)是一个提升性能的利器。

由于函数调用与目标机的应用二进制接口(ABI)强相关,静态编译器如GCC/LLVM的函数内联优化基本是在指令生成之前完成。

ZendVM的内联则发生在opcode生成后的FCALL指令的替换优化,pass id为16,其原理大致如下:

| 遍历op_array中的opcode,找到DO_XCALL四个opcode之一
| opcode ZEND_INIT_FCALL
| opcode ZEND_INIT_FCALL_BY_NAMEZ
     | 新建opcode,操作码置为ZEND_INIT_FCALL,计算栈大小,
        更新缓存槽位,析构常量池字面量,替换当前opline的opcode
| opcode ZEND_INIT_NS_FCALL_BY_NAME
     | 新建opcode,操作码置为ZEND_INIT_FCALL,计算栈大小,
        更新缓存槽位,析构常量池字面量,替换当前opline的opcode
| 尝试函数内联
     | 优化条件过滤 (每个优化pass通常有较多限制条件,某些场景下
         由于缺乏足够信息不能优化或出于代价考虑而排除) 
        | 方法调用ZEND_INIT_METHOD_CALL,直接返回不内联 | 引用传参,直接返回不内联 | 缺省参数为命名常量,直接返回不内联 | 被调用函数有返回值,添加一条ZEND_QM_ASSIGN赋值opcode | 被调用函数无返回值,插入一条ZEND_NOP空opcode | 删除调用被内联函数的call opcode(即当前online的前一条opcode) 
1234567891011121314151617

如下示例代码,当调用fname()时,使用字符串变量名fname来动态调用函数foo,而没有使用直接调用的方式。此时可通过VLD扩展查看其生成的opcode,或打开opcache调试选项(opcache.optdebuglevel=0xFFFFFFFF)亦可查看。

function foo() { }  
$fname = 'foo'; 
12

开启debug后dump可看出,发生函数调用优化前opcode序列(仅截取片段)为:

ASSIGN CV0($fname) string("foo")  
INIT_FCALL_BY_NAME 0 CV0($fname)  
DO_FCALL_BY_NAME  
123

INIT_FCALL_BY_NAME这条opcode执行逻辑较为复杂,当开启激进内联优化后,可将上述指令序列直接合并成一条DO_FCALL string("foo")指令,省去间接调用的开销。这样也恰好与直接调用生成的opcode一致。

4)如何为opcache opt添加一个优化pass

根据以上描述,可见向当前优化器加入一个pass并不会太难,大体步骤如下:

  • 先向zend_optimize优化器注册一个pass宏(例如添加pass17),并决定其优化级别。
  • 在优化管理器某个优化pass前后调用加入的pass(例如添加一个尾递归优化pass),建议在DFA/SSA分析pass之后添加,因为此时获得的优化信息更多。
  • 实现新加入的pass,进行定制代码转换(例如zendoptimizefunc_calls实现一个尾递归优化)。针对当前已有pass,主要添加转换pass,这里一般也可利用SSA/DFA的信息。不同于静态编译优化一般是在贴近于机器相关的低层中间表示优化,这里主要是在opcode层的opcode/operand相应的一些转换。
  • 实现pass前,与函数内联类似,通常首先收集优化所需信息,然后排除掉不适用该优化的一些场景(如非真正的尾不递归调用、参数问题无法做优化等)。实现优化后,可dump优化前后生成opcode结构的变化是否优化正确、是否符合预期(如尾递归优化最终的效果是变换函数调用为forloop的形式)。

4.一点思考

以下是对基于动态的PHP脚本程序执行的一些看法,仅供参考。

由于LLVM从前端到后端,从静态编译到jit整个工具链框架的支持,使得许多语言虚拟机都尝试整合。当前PHP7时代的ZendVM官方还没采用,原因之一虚拟机opcode承载着相当复杂的分析工作。相比于静态编译器的机器码每一条指令通常只干一件事情(通常是CPU指令时钟周期),opcode的操作数(operand)由于类型不固定,需要在运行期间做大量的类型检查、转换才能进行运算,这极度影响了执行效率。即使运行时采用jit,以byte code为单位编译,编译出的字节码也会与现有解释器一条一条opcode处理类似,类型需要处理、也不能把zval值直接存在寄存器。

以函数调用为例,比较现有的opcode执行与静态编译成机器码执行的区别,如下图:고도

类型推断

在不改变现有opcode设计的前提下,加强类型推断能力,进而为opcode的执行提供更多的类型信息,是提高执行性能的可选方法之一。

多层opcode

오피 같은 복잡한 분석 작업을 가정하므로, 오피 정규화 중간체 (중간 묘사, IR)을 나타내는 복수의 레이어로 분해 될 수있다. 각 프로그램은 중간 표현을 최적화하기 위해 선택할 수있는 층, 운반되는 정보의 양에 기초하여 종래의 컴파일러 중간 묘사, 높은 수준의 중간 표현 (HIR) 중간 중간 묘사합니다 (MIR)에, 하이 레벨 언어로 추상 머신 코드 부근 하부 중간 표현 (LIR).

패스 관리

로 이전에 책을 고래도에 설명 된 옵 코드 패스의 관리, 최적화, 개선의 여지가 있어야한다. 데이터 흐름 분석 / 제어 흐름 분석의 전류 의존성 최적화 분석은 여전히 ​​처리, 예컨대 관리 통과 시퀀스가 ​​실행될 때, 실행 시간, 등록 관리 복잡 패스 덤프 분석 정보 및 정지 프레임 성숙한 LLVM에 대하여 추천 사이로서, 누락 있지만 큰 차이.

산업 장관

ZendVM는 LLVM 실행하는 머신 코드로 컴파일 될 수있는 수단에 의해, 등 스위치의 종류 다수 zval 값을 얻을 수 있지만, 고속 확장 희생 컴파일 시간. 물론,도 libjit 사용.

추천

출처www.cnblogs.com/shixiuxian/p/11230939.html