理解Python之opcode及优化
什么是opcode
opcode又称为操作码,是将python源代码进行编译之后的结果,python虚拟机无法直接执行human-readable的源代码,因此python编译器第一步先将源代码进行编译,以此得到opcode。例如在执行python程序时一般会先生成一个pyc文件,pyc文件就是编译后的结果,其中含有opcode序列。
opcode初见
dis是python提供的对操作码进行分析的内置模块,下面由一个简单的示例程序来认识opcode:
def func(): a = 10 b = 20 c = a + b return cdis.dis(func)
结果输出内容如下,其中LOAD_CONST,STORE_FAST,BINARY_ADD即是我们提到的opcode,python是基于栈的语言,LOAD_CONST是将常量进行压栈,SOTRE_FAST是将栈顶元素赋值给参数指定的变量。python 2.7版中共计定义了约110个操作码,其中90以上的操作码需要参数,操作码定义参见opcode.h (https://www.jianshu.com/p/f540e540f940)。
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)
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>
- 再看由16进制表示的co_code序列,第一个Byte是0x64,是LOAD_CONST的操作码,由于LOAD_CONST含有参数,后面两个字节表示了LOAD_CONST的参数0100,由于使用big-endian的编码方式,因此0100就是1,而co_consts[1] 中存储的就是10。
- 再往后一个opcode是7d=125,指的是STORE_FAST的操作码,同样STORE_FAST后面需要一个参数(0000=0),即将栈顶值赋值给co_names存储的第0个元素(即a),至此完成了a = 10指令的处理。同理,后面6402007d0100即完成了b=20的操作。
- 完成两个赋值操作之后,紧接着是7c00007c0100,7C对应的操作码是LOAD_FAST,0000和0100分别是LOAD_FAST的参数,即从co_names中读取相应的两个元素压入栈中。
- 然后是指令0x17=23,表示操作码BINARY_ADD,由于23<90,因此BINARY_ADD不需要参数,该指令直接将栈顶的两个元素进行相加,并将两个元素出栈后再将结果放入栈顶。
- 接着是指令0x7d,即STORE_FAST,后面的参数为0200,对应co_names[2]表示的变量c,至此完成对c的赋值。
- 接着是0x7c0200,根据前面的内容可以知道是将co_names2压入栈中。
- 最的后0x53=83是RETURN_VALUE的操作码,由于小于90,因此也不需要操作,RETURN_VALUE只是将栈顶元素弹出,然后标记函数返回。
关于优化
python的目标不是一个性能高效的语言,出于脚本动态类型的原因虚拟机做了大量计算来判断一个变量的当前类型,并且整个python虚拟机是基于栈逻辑的,频繁的压栈出栈操作也需要大量计算。动态类型变化导致python的性能优化非常困难,尽管如此python在编译阶段还是在操作码层做了简单的peephole(窥空优化)。窥孔优化的原理比较简单,详情可以参见https://en.wikipedia.org/wiki/Peephole_optimization。这里举一个tuple相关的优化,更多的peephole相关的优化这里不作深入讨论。
a = (1, 2, 3, 4)
优化后的结果是:
2 0 LOAD_CONST 5 ((1, 2, 3, 4)) 3 STORE_FAST 0 (a) 6 LOAD_CONST 0 (None) 9 RETURN_VALUE
优化前的结果是:
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