来自 澳门威尼斯人平台 2020-01-16 12:28 的文章
当前位置: 澳门威尼斯人平台 > 澳门威尼斯人平台 > 正文

理解Python之opcode及优化

1.概述

PHP(本文所述案例PHP版本均为7.1.3)作为一门动态脚本语言,其在zend虚拟机执行过程为:读入脚本程序字符串,经由词法分析器将其转换为单词符号,接着语法分析器从中发现语法结构后生成抽象语法树,再经静态编译器生成opcode,最后经解释器模拟机器指令来执行每一条opcode。

在上述整个环节中,生成的opcode可以应用编译优化技术如死代码删除、条件常量传播、函数内联等各种优化来精简opcode,达到提高代码的执行性能的目的。

PHP扩展opcache,针对生成的opcode基于共享内存支持了缓存优化。在此基础上又加入了opcode的静态编译优化。这里所述优化通常采用优化器(Optimizer)来管理,编译原理中,一般用优化遍(Opt pass)来描述每一个优化。

整体上说,优化遍分两种:

  • 一种是分析pass,是提供数据流、控制流分析信息为转换pass提供辅助信息;
  • 一种是转换pass,它会改变生成代码,包括增删指令、改变替换指令、调整指令顺序等,通常每一个pass前后可dump出生成代码的变化。

本文基于编译原理,结合opcache扩展提供的优化器,以PHP编译基本单位op_array、PHP执行最小单位opcode为出发点。介绍编译优化技术在Zend虚拟机中的应用,梳理各个优化遍是如何一步步优化opcode来提高代码执行性能的。最后结合PHP语言虚拟机执行给出几点展望。

什么是opcode

opcode又称为操作码,是将python源代码进行编译之后的结果,python虚拟机无法直接执行human-readable的源代码,因此python编译器第一步先将源代码进行编译,以此得到opcode。例如在执行python程序时一般会先生成一个pyc文件,pyc文件就是编译后的结果,其中含有opcode序列。

PHP的核心组成,包括两部分 :ZEND引擎PHP内核。
Zend引擎:负责代码的解析、翻译和执行(解释器部分),负责具体功能的实现(功能性部分)。ZE也处理内存管理、变量作用域及调度程序调用。
PHP内核:负责同Web服务器的对话(接口部分,像sapi,cgi,cli等),负责具体功能的实现(功能性部分)。它绑定了SAPI层,通常涉及主机环境,并处理与它的通信。它同时对safe_mode和open_basedir的检测提供一致的控制层,就像流层将fopen()、fread()和fwrite()等用户空间的函数与文件和网络I/O联系起来一样。

2.几个概念说明

opcode初见

dis是python提供的对操作码进行分析的内置模块,下面由一个简单的示例程序来认识opcode:

def func():
    a = 10
    b = 20
    c = a + b
    return c
dis.dis(func)

 结果输出内容如下,其中LOAD_CONST,STORE_FAST,BINARY_ADD即是我们提到的opcode,python是基于栈的语言,LOAD_CONST是将常量进行压栈,SOTRE_FAST是将栈顶元素赋值给参数指定的变量。python 2.7版中共计定义了约110个操作码,其中90以上的操作码需要参数,操作码定义参见opcode.h ()。

  2           0 LOAD_CONST               1 (10)
              3 STORE_FAST               0 (a)

  3           6 LOAD_CONST               2 (20)
              9 STORE_FAST               1 (b)

  4          12 LOAD_FAST                0 (a)
             15 LOAD_FAST                1 (b)
             18 BINARY_ADD
             19 STORE_FAST               2 (c)

  5          22 LOAD_FAST                2 (c)
             25 RETURN_VALUE

 在解释opcode在python虚拟机的行为之前来认识一下PyCodeObject,python代码在编译完成后在内存中的对象称为PyCodeObject,PyCodeObject的C定义(python底层基于C语言)如下图:

typedef struct {
    PyObject_HEAD
    int co_argcount;             /* #arguments, except *args */
    int co_kwonlyargcount;       /* #keyword only arguments */
    int co_nlocals;              /* #local variables */
    int co_stacksize;            /* #entries needed for evaluation stack */
    int co_flags;                /* CO_..., see below */
    PyObject *co_code;           /* instruction opcodes */
    PyObject *co_consts;         /* list (constants used) */
    PyObject *co_names;          /* list of strings (names used) */
    PyObject *co_varnames;       /* tuple of strings (local variable names) */
    PyObject *co_freevars;       /* tuple of strings (free variable names) */
    PyObject *co_cellvars;       /* tuple of strings (cell variable names) */
    /* The rest doesn't count for hash or comparisons */
    unsigned char *co_cell2arg;  /* Maps cell vars which are arguments. */
    PyObject *co_filename;       /* unicode (where it was loaded from) */
    PyObject *co_name;           /* unicode (name, for reference) */
    int co_firstlineno;          /* first source line number */
    PyObject *co_lnotab;         /* string (encoding addr<->lineno mapping) See
                                    Objects/lnotab_notes.txt for details. */
    void *co_zombieframe;        /* for optimization only (see frameobject.c) */
    PyObject *co_weakreflist;    /* to support weakrefs to code objects */
} PyCodeObject;

其中这里面我们关心co_consts和co_names两个列表,第一个存放了所有的常量,第二存放了所有的变量,因此有下面的结论。

LOAD_CONST 0 表示将co_consts中的第0个(下标0)放入栈中。
STORE_FAST 0 表示将栈顶元素赋值给co_names中存放的第0个元素。

 有了上面的知识很容易理解出下面操作码序列所表示的内容 c=a+b:

             12 LOAD_FAST                0 (a)
             15 LOAD_FAST                1 (b)
             18 BINARY_ADD
             19 STORE_FAST               2 (c)

PHP生存周期

1)静态编译/解释执行/即时编译

静态编译(static compilation),也称事前编译(ahead-of-time compilation),简称AOT。即把源代码编译成目标代码,执行时在支持目标代码的平台上运行。

动态编译(dynamic compilation),相对于静态编译而言,指”在运行时进行编译”。通常情况下采用解释器(interpreter)编译执行,它是指一条一条的解释执行源语言。

JIT编译(just-in-time compilation),即即时编译,狭义指某段代码即将第一次被执行时进行编译,而后则不用编译直接执行,它为动态编译的一种特例。

上述三类不同编译执行流程,可大体如下图来描述:

澳门威尼斯人平台 1

co_code中存储了操作码序列,编译好的操作码以二进制的方式进行存储,co_code

[(opcode}[args{0,1}]+的形式,其中opcode占用一个byte,编号90以下的操作码不需要参数,90及以上的操作码需要两个byte的args,下面是func函数编译之后得到的PyCodeObject信息,这里https://github.com/yukunxie/PythonCodeObjectParser/blob/master/codeparser.py提供了一下PyCodeObject的查看工具。

        <item idx="0" name="func" type="codeobject">
            <co_consts count="3">
                <item idx="0">None</item>
                <item idx="1">10</item>
                <item idx="2">20</item>
            </co_consts>
            <co_names count="0"/>
            <co_varnames count="3">
                <name idx="0">a</name>
                <name idx="1">b</name>
                <name idx="2">c</name>
            </co_varnames>
            <co_cellvars count="0"/>
            <co_freevars count="0"/>
            <co_filename>code.py</co_filename>
            <co_ename>func</co_ename>
            <co_nlocals>3</co_nlocals>
            <co_stacksize>2</co_stacksize>
            <co_argcount>0</co_argcount>
            <co_code>6401007d00006402007d01007c00007c0100177d02007c020053</co_code>
        </item>
  1. 再看由16进制表示的co_code序列,第一个Byte是0x64,是LOAD_CONST的操作码,由于LOAD_CONST含有参数,后面两个字节表示了LOAD_CONST的参数0100,由于使用big-endian的编码方式,因此0100就是1,而co_consts[1] 中存储的就是10。
  2. 再往后一个opcode是7d=125,指的是STORE_FAST的操作码,同样STORE_FAST后面需要一个参数(0000=0),即将栈顶值赋值给co_names存储的第0个元素(即a),至此完成了a = 10指令的处理。同理,后面6402007d0100即完成了b=20的操作。
  3. 完成两个赋值操作之后,紧接着是7c00007c0100,7C对应的操作码是LOAD_FAST,0000和0100分别是LOAD_FAST的参数,即从co_names中读取相应的两个元素压入栈中。
  4. 然后是指令0x17=23,表示操作码BINARY_ADD,由于23<90,因此BINARY_ADD不需要参数,该指令直接将栈顶的两个元素进行相加,并将两个元素出栈后再将结果放入栈顶。
  5. 接着是指令0x7d,即STORE_FAST,后面的参数为0200,对应co_names[2]表示的变量c,至此完成对c的赋值。
  6. 接着是0x7c0200,根据前面的内容可以知道是将co_names2压入栈中。
  7. 最的后0x53=83是RETURN_VALUE的操作码,由于小于90,因此也不需要操作,RETURN_VALUE只是将栈顶元素弹出,然后标记函数返回。
  • 模块初始化:SAPI,CGI,CLI启动时,PHP_MINIT_FUNCTION
  • 请求初始化: 接收到client请求时,PHP_RINIT_FUNCTION
  • 执行php脚本
  • 结束请求: 请求执行完毕,PHP_RSHUTDOWN_FUNCTION, ZE完成内存的回收和变量的清理
  • 关闭模块:服务器关闭或者脚本执行完毕时,PHP_MSHUTDOWN_FUNCTION

2)数据流/控制流

编译优化需要从程序中获取足够多的信息,这是所有编译优化的根基。

编译器前端产生的结果可以是语法树亦可以是某种低级中间代码。但无论结果什么形式,它对程序做什么、如何做仍然没有提供多少信息。编译器将发现每一个过程内控制流层次结构的任务留给控制流分析,将确定与数据处理有关的全局信息任务留给数据流分析。

  • 控制流 是获取程序控制结构信息的形式化分析方法,它为数据流分析、依赖分析的基础。控制的一个基本模型是控制流图(Control Flow Graph,CFG)。单一过程的控制流分析有使用必经结点找循环、区间分析两种途径。
  • 数据流 从程序代码中收集程序的语义信息,并通过代数的方法在编译时确定变量的定义和使用。数据的一个基本模型是数据流图(Data Flow Graph,DFG)。通常的数据流分析是基于控制树的分析(Control-tree-based data-flow analysis),算法分为区间分析与结构分析两种。

关于优化

python的目标不是一个性能高效的语言,出于脚本动态类型的原因虚拟机做了大量计算来判断一个变量的当前类型,并且整个python虚拟机是基于栈逻辑的,频繁的压栈出栈操作也需要大量计算。动态类型变化导致python的性能优化非常困难,尽管如此python在编译阶段还是在操作码层做了简单的peephole(窥空优化)。窥孔优化的原理比较简单,详情可以参见https://en.wikipedia.org/wiki/Peephole_optimization。这里举一个tuple相关的优化,更多的peephole相关的优化这里不作深入讨论。

a = (1, 2, 3, 4)

本文相关链接

3)op_array

类似于C语言的栈帧(stack frame)概念,即一个运行程序的基本单位(一帧),一般为一次函数调用的基本单位。此处,一个函数或方法、整个PHP脚本文件、传给eval表示PHP代码的字符串都会被编译成一个op_array。

实现上op_array为一个包含程序运行基本单位的所有信息的结构体,当然opcode数组为该结构最为重要的字段,不过除此之外还包含变量类型、注释信息、异常捕获信息、跳转信息等。

优化后的结果是:
  2           0 LOAD_CONST               5 ((1, 2, 3, 4))
              3 STORE_FAST               0 (a)
              6 LOAD_CONST               0 (None)
              9 RETURN_VALUE
  • 深入理解php内核 编写扩展 I:介绍PHP和Zend

4)opcode

解释器执行(ZendVM)过程即是执行一个基本单位op_array内的最小优化opcode,按顺序遍历执行,执行当前opcode,会预取下一条opcode,直到最后一个RETRUN这个特殊的opcode返回退出。

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

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

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

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

澳门威尼斯人平台 2

优化前的结果是:
  2           0 LOAD_CONST               1 (1)
              3 LOAD_CONST               2 (2)
              6 LOAD_CONST               3 (3)
              9 LOAD_CONST               4 (4)
             12 BUILD_TUPLE              4
             15 STORE_FAST               0 (a)
             18 LOAD_CONST               0 (None)
             21 RETURN_VALUE

本文由澳门威尼斯人平台发布于澳门威尼斯人平台,转载请注明出处:理解Python之opcode及优化

关键词: