Python内核源码解析:OpCode字节码优化中

Python 字节码优化核心技术为窥孔优化**(Peephole Optimization)**。这是一种用滑动窗口实现的局部优化手段,窗口的大小决定了每一次优化的长度,窗口不断滑动,直到遍历完所有的字节码,反复几次操作,完成全部优化工作。

执行优化手段的目的是为了减少执行指令,或者将一个或者多个指令替换为更高效的指令,从而达到提升程序执行效率的目的。

当我们知道窥孔优化发生在字节码编译之后,字节码执行之前(也就是语法树生成之后)时,这就更加方便我们验证字节码优化到底做了什么?我们可以尝试输出 Python 的抽象语法树(AST),打印编译之后的字节码。如果程序没有优化,语法树与字节码应该存在对应关系,至少存在语法数的每一个节点都能在字节码集合中找到对应的字节码。而优化过的字节码有可能存在字节码被优化移除,或者被替换的现象。

窥孔优化中常用的技术

  1. 空序列 - 删除无用的操作
  2. 组合操作 - 用一项等价操作替换多项操作
  3. 代数定律 -- 使用代数定律来简化或重新排序指令
  4. 特殊情况指令 -- 使用为特殊操作数情况设计的指令
  5. 地址模式操作 -- 使用地址模式来简化代码

回顾

Python 内核源码解析:OpCode 字节码优化上》中简单的介绍了字节码优化的相关函数逻辑,以及如何清理无用的代码等。

1. 带着问题,了解窥孔优化

光阅读 Python 编译字节码的内核源码 Python/compile.c,我们很难了解字节码编译到底做了什么?什么样的 Python 代码才会触发到窥孔优化?源码文件 Lib/test/test_peepholer.py 包含了窥孔优化相关的测试用例,它用来验证字节码编译优化的正确性,我们将通过这些测试用例来推测窥孔优化做了哪些工作。

UNARY_NOT POP_JUMP_IF_FALSE --> POP_JUMP_IF_TRUE 两个指令变成一个指令

python 复制代码
    def test_unot(self):
        # UNARY_NOT POP_JUMP_IF_FALSE  -->  POP_JUMP_IF_TRUE'
        def unot(x):
            if not x == 2:
                del x
        self.assertNotInBytecode(unot, 'UNARY_NOT')
        self.assertNotInBytecode(unot, 'POP_JUMP_IF_FALSE')
        self.assertInBytecode(unot, 'POP_JUMP_IF_TRUE')
        self.check_lnotab(unot)

测试用例 test_unot 是验证两个指令 UNARY_NOT 和 POP_JUMP_IF_FALSE 替换成一个指令 POP_JUMP_IF_TRUE 的正确性。

接下来我们推测一下自己码编译优化做了哪些工作。首先符号 == 的优先级是大于 not 的,如果没有关键词 not,程序 if 判断表达式最终生成的指令是 POP_JUMP_IF_FALSE。如果前面加上 not,对 if 表达式的结果取反(UNARY_NOT),这种操作其实是有优化空间的,可以直接用 POP_JUMP_IF_TRUE 来代替 UNARY_NOT 和 POP_JUMP_IF_FALSE 这组指令。

2. 接下来用 Python 代码验证一下我们的推测

验证 not 和 == 的优先级

毋庸置疑,not x == 2 其实就是 not (x == 2),也就是对 (x == 2) 的值取反。

python 复制代码
>>> not 2 == 2
False
>>> not 1 == False
True
>>> not 1
False
>>> not True == False
True

3. 生成表达式 if 的字节码(表达式中没有 not 关键字)

表达式 if 中没有 not 关键字,其生成的跳转指令为:POP_JUMP_IF_FALSE,这与我们之前的推测一致。

python 复制代码
>>> import dis
>>> def test_if_false(x):
    if x == 2:
        del x... ...
...
>>> dis.dis(test_if_false.__code__)
  2           0 LOAD_FAST                0 (x)
              2 LOAD_CONST               1 (2)
              4 COMPARE_OP               2 (==)
              6 POP_JUMP_IF_FALSE       10

  3           8 DELETE_FAST              0 (x)
        >>   10 LOAD_CONST               0 (None)
             12 RETURN_VALUE

4. 验证组合指令 UNARY_NOT 和 POP_JUMP_IF_FALSE 优化为一个指令 POP_JUMP_IF_TRUE

加上 not 关键字之后,POP_JUMP_IF_FALSE 消失了,转而出现的是 POP_JUMP_IF_TRUE。又一次验证了我们的推测是正确的。

python 复制代码
>>> def unot(x):
            if not x == 2:
                del x... ...
...
>>> import dis
>>> dis.dis(unot.__code__)
  2           0 LOAD_FAST                0 (x)
              2 LOAD_CONST               1 (2)
              4 COMPARE_OP               2 (==)
              6 POP_JUMP_IF_TRUE        10

  3           8 DELETE_FAST              0 (x)
        >>   10 LOAD_CONST               0 (None)
             12 RETURN_VALUE

5. 最终确定这个优化不是发生在 AST 优化阶段

从如下 AST 输出可以看出,节点 UnaryOp 仍然存在,说明上面的优化不是在 AST 生成阶段,而是发生在字节码编译优化阶段。

py 复制代码
>>> import ast
>>> 
>>> code = """\
... def unot(x):
...     if not x == 2:
...         del x
... """
>>> print(ast.dump(ast.parse(code, mode='exec'), indent=4))
Module(
    body=[
        FunctionDef(
            name='unot',
            args=arguments(
                posonlyargs=[],
                args=[
                    arg(arg='x')],
                kwonlyargs=[],
                kw_defaults=[],
                defaults=[]),
            body=[
                If(
                    test=UnaryOp(
                        op=Not(),
                        operand=Compare(
                            left=Name(id='x', ctx=Load()),
                            ops=[
                                Eq()],
                            comparators=[
                                Constant(value=2)])),
                    body=[
                        Delete(
                            targets=[
                                Name(id='x', ctx=Del())])],
                    orelse=[])],
            decorator_list=[])],
    type_ignores=[])
>>> 

总结

从上面的例子,我们通过程序各个阶段的输入与输出,推测中间部分的实现逻辑,又通过相关代码的辅助输出,一次又一次的验证了我们的推测。大的问题可以拆分为一个个小的问题来解决,最后看起来大的问题也没有那么复杂。

相关推荐
这个男人是小帅5 分钟前
【GAT】 代码详解 (1) 运行方法【pytorch】可运行版本
人工智能·pytorch·python·深度学习·分类
爱吃生蚝的于勒1 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
小白学大数据3 小时前
Python爬虫开发中的分析与方案制定
开发语言·c++·爬虫·python
Shy9604184 小时前
Doc2Vec句子向量
python·语言模型
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
ChoSeitaku6 小时前
链表循环及差集相关算法题|判断循环双链表是否对称|两循环单链表合并成循环链表|使双向循环链表有序|单循环链表改双向循环链表|两链表的差集(C)
c语言·算法·链表
DdddJMs__1356 小时前
C语言 | Leetcode C语言题解之第557题反转字符串中的单词III
c语言·leetcode·题解
秀儿还能再秀7 小时前
机器学习——简单线性回归、逻辑回归
笔记·python·学习·机器学习
娃娃丢没有坏心思7 小时前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++
阿_旭8 小时前
如何使用OpenCV和Python进行相机校准
python·opencv·相机校准·畸变校准