理解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>
  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只是将栈顶元素弹出,然后标记函数返回。

关于优化

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
对比优化前后的结果可以发现,优化后明显减少了指令的数量,Peephole优化只能对tuple进行优化,而对dict和list则无法进行优化,因为tuple属于在运行时不会发生变化的结构,可以存储于co_consts中,而list和dict则在运行时可以被更改,无法存储于co_consts中。
(0)

相关推荐

  • 日常代码笔记,python的推导式&性能评估

    写代码跟写作类似,需要不断地练习,不断地阅读,获得灵感,然后反复修改(重构).写代码有代码补全工具,然后我们还是需要不断地练习.实验自己的新想法. 之前对python的推导式没有仔细去了解,今天抽空练 ...

  • Python 为什么引入这两个关键词

    啥是 global 和 nonlocal Python 支持的关键词里,global 和 nonlocal 初学者接触的少,不知道是做什么用的:一些人虽然知道它们的作用,但对为什么要引入这两个关键词则 ...

  • Python中tuple和list的区别?基础学习!

    想必大家都知道,Python数据类型有很多种,其中有两个对象的写法非常相似,它就是tuple元组和list列表,让人傻傻分不清楚.那么你知道Python中tuple和list有什么区别吗?我们来看看具 ...

  • 编写高质量代码:改善Python程序的91个建议.1

    人生苦短,睡觉最好! -U 是--upgrade的缩写,如果以已经安装就升级到最新版 先得安装一下 输出的没毛病 我们实验一下 我提前把代码改过 pep8 --show-source --show-p ...

  • python元组

    元组tuple也是python常用的一种数据类型,与列表类似,唯一不同的是元组中的元素是不允许修改的. 元组使用的是小括号(),列表使用的是中括号[]. 1.元组的创建 元组的创建和列表一样,只需要将 ...

  • 如何理解Python之禅著名的格言:显式胜于隐式?

    作者:三点水 来源:https://lotabout.me/2021/Explicit-is-Better-than-implicit "Explicit is better than im ...

  • 理解Python多线程:通过易懂的小例子展开

    4天前 1 默认启动主线程 一般的,程序默认执行只在一个线程,这个线程称为主线程,例子演示如下: 导入线程相关的模块 threading: import threading threading的类方法 ...

  • selenium+python自动化81-报告优化

    一. 优化html报告 为了满足小伙伴的各种变态需求,为了装逼提示逼格,为了让报告更加高大上,测试报告做了以下优化: - 测试报告中文显示,优化一些断言失败正文乱码问题 - 新增错误和失败截图,展示到 ...

  • python笔记31-ddt报告优化

    前言 使用ddt框架生成html报告的时候出现dict() -> new empty dictionary dict(mapping) -> new dictionary initiali ...

  • Python基于粒子群优化的投资组合优化研究

    原文链接:http://tecdat.cn/?p=6811 我今年的研究课题是使用粒子群优化(PSO)的货币进行交易组合优化.在本文中,我将介绍投资组合优化并解释其重要性.其次,我将演示粒子群优化如何 ...

  • 深入理解 Python 内部函数和闭包(进阶)

    大家好,我是安果! 本文以内部函数为主线,深入讲解内部函数和闭包的应用场景和原理,学会后你的 Python 水平会再上一个台阶,对工作面试或实战应用都会很有帮助 本文包括: 函数是一等公民 内部函数定 ...

  • 面向对象: 理解python类的单继承与多继承

    俗话说"龙生龙凤生凤老鼠的孩子会打洞",每种动物都有各自的特性,比如 老鼠会打洞 猫会爬树 鲨鱼会游泳 不同种类有不同的天性.而在程序员的思维中,动物是对象, 天性是这个类方法或者 ...

  • 一文理解 Python 中的变量

    " 变量让程序活起来,不再千人一面." 我们在之前的文章<Python 基本数据类型介绍>中了解了如何创建各种基本类型的数据,但是我们的例子中使用的都是"字面 ...

  • 【Python基础】一文理解Python集合,17个方法全解,看完就够了

    一.集合的定义 01 定义与特性 Python中的集合类似于数学中的集合概念,它是一组无序.不可重复元素序列,集合用{value1,value2}创建,某种程度上可以把集合看作是没有值的字典.字典是d ...