19.4. dis — 反汇编 Python 字节码
目的:将代码对象转换为字节码的人类可读表示形式以供分析。
dis 模块包括用于反编译 Python 字节码将其拆解为更易于阅读的形式的函数。 查看解释器正在执行的字节码是手动调整紧密循环和执行其他类型优化的好方法。 它对于在多线程应用程序中查找竞争条件也很有用,因为它可用于估计代码中线程控制可能切换的点。
警告
字节码的使用是 CPython 解释器的特定版本的实现细节。 请参阅源代码中的
Include/opcode.h,了解用于当前查找字节码规范列表的解释器版本。
基本反编译
函数 dis() 打印 Python 代码源(模块、类、方法、函数或代码对象)的反汇编形式。 可以通过从命令行运行 dis 来反汇编诸如 dis_simple.py 之类的模块。
dis_simple.py
#!/usr/bin/env python3
# encoding: utf-8
my_dict = {'a': 1}
输出被组织成列,包含原始源行号,代码对象中的指令地址、操作码名称以及传递给操作码的任何参数。
$ python3 -m dis dis_simple.py
  4           0 LOAD_CONST               0 ('a')
              2 LOAD_CONST               1 (1)
              4 BUILD_MAP                1
              6 STORE_NAME               0 (my_dict)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE
在这个例子中,源转换为四种不同的操作来创建和填充字典,然后将结果保存到局部变量。 由于 Python 解释器是基于堆栈的,因此第一步是使用 LOAD_CONST 以正确的顺序将常量放入堆栈,然后使用 BUILD_MAP 弹出新的键和值以添加到字典中。 生成的 dict 对象绑定到变量名 my_dict 并带有 STORE_NAME 。
函数反编译
不幸的是,反编译整个模块并不会自动递归到函数中。
dis_function.py
#!/usr/bin/env python3
# encoding: utf-8
def f(*args):
    nargs = len(args)
    print(nargs, args)
if __name__ == '__main__':
    import dis
    dis.dis(f)
反编译 dis_function.py 的结果显示了将函数的代码对象加载到堆栈然后将其转换为函数( LOAD_CONST , MAKE_FUNCTION )的操作,但不是函数的主体。
$ python3 -m dis dis_function.py
  5           0 LOAD_CONST               0 (<code object f at
0x1044fcf60, file "dis_function.py", line 5>)
              2 LOAD_CONST               1 ('f')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)
 10           8 LOAD_NAME                1 (__name__)
             10 LOAD_CONST               2 ('__main__')
             12 COMPARE_OP               2 (==)
             14 POP_JUMP_IF_FALSE       34
 11          16 LOAD_CONST               3 (0)
             18 LOAD_CONST               4 (None)
             20 IMPORT_NAME              2 (dis)
             22 STORE_NAME               2 (dis)
 12          24 LOAD_NAME                2 (dis)
             26 LOAD_ATTR                2 (dis)
             28 LOAD_NAME                0 (f)
             30 CALL_FUNCTION            1
             32 POP_TOP
        >>   34 LOAD_CONST               4 (None)
             36 RETURN_VALUE
要查看函数内部,函数本身必须传递给 dis() 。
$ python3 dis_function.py
  6           0 LOAD_GLOBAL              0 (len)
              2 LOAD_FAST                0 (args)
              4 CALL_FUNCTION            1
              6 STORE_FAST               1 (nargs)
  7           8 LOAD_GLOBAL              1 (print)
             10 LOAD_FAST                1 (nargs)
             12 LOAD_FAST                0 (args)
             14 CALL_FUNCTION            2
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE
要打印函数的摘要,包括有关它使用的参数和名称的信息,请调用 show_code() ,将函数作为第一个参数传递。
#!/usr/bin/env python3
# encoding: utf-8
def f(*args):
    nargs = len(args)
    print(nargs, args)
if __name__ == '__main__':
    import dis
    dis.show_code(f)
show_code() 的参数传递给 code_info() ,它返回一个格式正确的函数、方法、代码字符串或其他代码对象的摘要,准备好打印。
$ python3 dis_show_code.py
Name:              f
Filename:          dis_show_code.py
Argument count:    0
Kw-only arguments: 0
Number of locals:  2
Stack size:        3
Flags:             OPTIMIZED, NEWLOCALS, VARARGS, NOFREE
Constants:
   0: None
Names:
   0: len
   1: print
Variable names:
   0: args
   1: nargs
类
类可以传递给 dis() ,在这种情况下,所有的方法都会被反编译。
dis_class.py
#!/usr/bin/env python3
# encoding: utf-8
import dis
class MyObject:
    """Example for dis."""
    CLASS_ATTRIBUTE = 'some value'
    def __str__(self):
        return 'MyObject({})'.format(self.name)
    def __init__(self, name):
        self.name = name
dis.dis(MyObject)
这些方法按字母顺序列出,而不是它们在文件中出现的顺序。
$ python3 dis_class.py
Disassembly of __init__:
 16           0 LOAD_FAST                1 (name)
              2 LOAD_FAST                0 (self)
              4 STORE_ATTR               0 (name)
              6 LOAD_CONST               0 (None)
              8 RETURN_VALUE
Disassembly of __str__:
 13           0 LOAD_CONST               1 ('MyObject({})')
              2 LOAD_ATTR                0 (format)
              4 LOAD_FAST                0 (self)
              6 LOAD_ATTR                1 (name)
              8 CALL_FUNCTION            1
             10 RETURN_VALUE
源代码
使用程序的源代码通常比使用代码对象本身更方便。 dis 中的函数接受包含源代码的字符串参数,并在生成反编译或其他输出之前将它们转换为代码对象。
dis_string.py
import dis
code = """
my_dict = {'a': 1}
"""
print('Disassembly:\n')
dis.dis(code)
print('\nCode details:\n')
dis.show_code(code)
传递字符串可以保存编译代码的步骤并自己保存对结果的引用,这在检查函数外部的语句时更为方便。
$ python3 dis_string.py
Disassembly:
  2           0 LOAD_CONST               0 ('a')
              2 LOAD_CONST               1 (1)
              4 BUILD_MAP                1
              6 STORE_NAME               0 (my_dict)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE
Code details:
Name:              <module>
Filename:          <disassembly>
Argument count:    0
Kw-only arguments: 0
Number of locals:  0
Stack size:        2
Flags:             NOFREE
Constants:
   0: 'a'
   1: 1
   2: None
Names:
   0: my_dict
使用反编译调试
有时在调试异常时,查看哪个字节码导致问题会很有用。 有几种方法可以围绕错误反编译代码。 第一种是通过在交互式解释器中使用 dis() 来报告最后一个异常。 如果没有参数传递给 dis() ,那么它会查找异常并显示导致错误的堆栈顶部的反编译。
$ python3
Python 3.5.1 (v3.5.1:37a07cee5969, Dec  5 2015, 21:12:44)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>> j = 4
>>> i = i + 4
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'i' is not defined
>>> dis.dis()
  1 -->       0 LOAD_NAME                0 (i)
              3 LOAD_CONST               0 (4)
              6 BINARY_ADD
              7 STORE_NAME               0 (i)
             10 LOAD_CONST               1 (None)
             13 RETURN_VALUE
>>>
行号后面的 --> 表示导致错误的操作码。 没有定义 i 变量,因此与名称关联的值无法加载到堆栈中。
程序还可以直接将其传递给 distb() 来打印有关活动回溯的信息。 在这个例子中,有一个 DivideByZero 异常,但由于公式有两个分区,因此可能不清楚哪个部分为零。
dis_traceback.py
#!/usr/bin/env python3
# encoding: utf-8
i = 1
j = 0
k = 3
try:
    result = k * (i / j) + (i / k)
except Exception:
    import dis
    import sys
    exc_type, exc_value, exc_tb = sys.exc_info()
    dis.distb(exc_tb)
在反编译版本中将错误加载到堆栈中时,很容易发现该错误。 使用 --> 突出显示错误操作,其中前一行将 j 的值压入堆栈。
    $ python3 dis_traceback.py
      4           0 LOAD_CONST               0 (1)
                  2 STORE_NAME               0 (i)
      5           4 LOAD_CONST               1 (0)
                  6 STORE_NAME               1 (j)
      6           8 LOAD_CONST               2 (3)
                 10 STORE_NAME               2 (k)
      8          12 SETUP_EXCEPT            24 (to 38)
      9          14 LOAD_NAME                2 (k)
                 16 LOAD_NAME                0 (i)
                 18 LOAD_NAME                1 (j)
        -->      20 BINARY_TRUE_DIVIDE
                 22 BINARY_MULTIPLY
                 24 LOAD_NAME                0 (i)
                 26 LOAD_NAME                2 (k)
                 28 BINARY_TRUE_DIVIDE
                 30 BINARY_ADD
                 32 STORE_NAME               3 (result)
...trimmed...
循环性能分析
除了调试错误,dis 还可以帮助识别性能问题。 检查反编译代码对于紧密循环特别有用,其中的 Python 指令数量很少但它们被转换为低效的字节码集。 通过检查类的一些不同实现,可以看到反编译的有用性, Dictionary 读取单词列表并按照第一个字母对它们进行分组。
dis_test_loop.py
import dis
import sys
import textwrap
import timeit
module_name = sys.argv[1]
module = __import__(module_name)
Dictionary = module.Dictionary
dis.dis(Dictionary.load_data)
print()
t = timeit.Timer(
    'd = Dictionary(words)',
    textwrap.dedent("""
    from {module_name} import Dictionary
    words = [
        l.strip()
        for l in open('/usr/share/dict/words', 'rt')
    ]
    """).format(module_name=module_name)
)
iterations = 10
print('TIME: {:0.4f}'.format(t.timeit(iterations) / iterations))
测试驱动程序应用程序 dis_test_loop.py 可用于运行 Dictionary 类的每个实体,从一个简单但缓慢的实现开始。
dis_slow_loop.py
#!/usr/bin/env python3
# encoding: utf-8
class Dictionary:
    def __init__(self, words):
        self.by_letter = {}
        self.load_data(words)
    def load_data(self, words):
        for word in words:
            try:
                self.by_letter[word[0]].append(word)
            except KeyError:
                self.by_letter[word[0]] = [word]
使用此版本运行测试程序会显示反编译的程序以及运行所需的时间。
$ python3 dis_test_loop.py dis_slow_loop
 12           0 SETUP_LOOP              83 (to 86)
              3 LOAD_FAST                1 (words)
              6 GET_ITER
        >>    7 FOR_ITER                75 (to 85)
             10 STORE_FAST               2 (word)
 13          13 SETUP_EXCEPT            28 (to 44)
 14          16 LOAD_FAST                0 (self)
             19 LOAD_ATTR                0 (by_letter)
             22 LOAD_FAST                2 (word)
             25 LOAD_CONST               1 (0)
             28 BINARY_SUBSCR
             29 BINARY_SUBSCR
             30 LOAD_ATTR                1 (append)
             33 LOAD_FAST                2 (word)
             36 CALL_FUNCTION            1 (1 positional, 0
keyword pair)
             39 POP_TOP
             40 POP_BLOCK
             41 JUMP_ABSOLUTE            7
 15     >>   44 DUP_TOP
             45 LOAD_GLOBAL              2 (KeyError)
             48 COMPARE_OP              10 (exception match)
             51 POP_JUMP_IF_FALSE       81
             54 POP_TOP
             55 POP_TOP
             56 POP_TOP
 16          57 LOAD_FAST                2 (word)
             60 BUILD_LIST               1
             63 LOAD_FAST                0 (self)
             66 LOAD_ATTR                0 (by_letter)
             69 LOAD_FAST                2 (word)
             72 LOAD_CONST               1 (0)
             75 BINARY_SUBSCR
             76 STORE_SUBSCR
             77 POP_EXCEPT
             78 JUMP_ABSOLUTE            7
        >>   81 END_FINALLY
             82 JUMP_ABSOLUTE            7
        >>   85 POP_BLOCK
        >>   86 LOAD_CONST               0 (None)
             89 RETURN_VALUE
TIME: 0.0568
上一个输出显示 dis_slow_loop.py 需要0.0568秒才能在 OS X 上的/usr/share/dict/words 副本中加载 235886 个单词。这不是太糟糕,但随附的反编译表明循环在做更多它不需要的工作。 当它进入操作码 13 中的循环时,它会建立一个异常上下文( SETUP_EXCEPT )。 然后,在将 word 附加到列表之前,需要六个操作码才能找到 self.by_letter[word[0]] 。 如果因为 word[0] 不在字典中而有异常,则异常处理程序执行所有相同的工作以确定 word [0] (三个操作码)并设置 self.by_letter[word[0]] 到包含该单词的新列表。
消除异常设置的一种技术是预先填充 self.by_letter ,其中每个字母的字母都有一个列表。 这意味着应始终能找到新单词的列表,并且可以在查找后保存该值。
dis_faster_loop.py
#!/usr/bin/env python3
# encoding: utf-8
import string
class Dictionary:
    def __init__(self, words):
        self.by_letter = {
            letter: []
            for letter in string.ascii_letters
        }
        self.load_data(words)
    def load_data(self, words):
        for word in words:
            self.by_letter[word[0]].append(word)
此更改将操作码数量减少了一半,但只将时间减少到 0.0567 秒。 显然,异常处理有一些开销,但不是很大。
$ python3 dis_test_loop.py dis_faster_loop
 17           0 SETUP_LOOP              38 (to 41)
              3 LOAD_FAST                1 (words)
              6 GET_ITER
        >>    7 FOR_ITER                30 (to 40)
             10 STORE_FAST               2 (word)
 18          13 LOAD_FAST                0 (self)
             16 LOAD_ATTR                0 (by_letter)
             19 LOAD_FAST                2 (word)
             22 LOAD_CONST               1 (0)
             25 BINARY_SUBSCR
             26 BINARY_SUBSCR
             27 LOAD_ATTR                1 (append)
             30 LOAD_FAST                2 (word)
             33 CALL_FUNCTION            1 (1 positional, 0
keyword pair)
             36 POP_TOP
             37 JUMP_ABSOLUTE            7
        >>   40 POP_BLOCK
        >>   41 LOAD_CONST               0 (None)
             44 RETURN_VALUE
TIME: 0.0567
通过在循环之外移动 self.by_letter 的查找,可以进一步提高性能(毕竟值不会改变)。
dis_fastest_loop.py
#!/usr/bin/env python3
# encoding: utf-8
import collections
class Dictionary:
    def __init__(self, words):
        self.by_letter = collections.defaultdict(list)
        self.load_data(words)
    def load_data(self, words):
        by_letter = self.by_letter
        for word in words:
            by_letter[word[0]].append(word)
操作码 0-6 现在找到 self.by_letter 的值并将其保存为局部变量 by_letter 。 使用局部变量只需要一个操作码,而不是两个操作码(语句 22 使用 LOAD_FAST 将字典放入堆栈中)。此更改后,运行时间将减少到0.0473秒。
$ python3 dis_test_loop.py dis_fastest_loop
 14           0 LOAD_FAST                0 (self)
              3 LOAD_ATTR                0 (by_letter)
              6 STORE_FAST               2 (by_letter)
 15           9 SETUP_LOOP              35 (to 47)
             12 LOAD_FAST                1 (words)
             15 GET_ITER
        >>   16 FOR_ITER                27 (to 46)
             19 STORE_FAST               3 (word)
 16          22 LOAD_FAST                2 (by_letter)
             25 LOAD_FAST                3 (word)
             28 LOAD_CONST               1 (0)
             31 BINARY_SUBSCR
             32 BINARY_SUBSCR
             33 LOAD_ATTR                1 (append)
             36 LOAD_FAST                3 (word)
             39 CALL_FUNCTION            1 (1 positional, 0
keyword pair)
             42 POP_TOP
             43 JUMP_ABSOLUTE           16
        >>   46 POP_BLOCK
        >>   47 LOAD_CONST               0 (None)
             50 RETURN_VALUE
TIME: 0.0473
Brandon Rhodes 建议的进一步优化是完全消除 for 循环的 Python 版本。 如果 itertools.groupby() 用于排列输入,则迭代将移至 C 中。这是安全的,因为已知输入已排序。 如果不是这样,程序将需要先对它们进行排序。
dis_eliminate_loop.py
#!/usr/bin/env python3
# encoding: utf-8
import operator
import itertools
class Dictionary:
    def __init__(self, words):
        self.by_letter = {}
        self.load_data(words)
    def load_data(self, words):
        # 按字母表排列
        grouped = itertools.groupby(
            words,
            key=operator.itemgetter(0),
        )
        # 保存已排序的单词集
        self.by_letter = {
            group[0][0]: group
            for group in grouped
        }
itertools 版本只需要0.0332秒运行,大约是原始运行时间的60%。
    $ python3 dis_test_loop.py dis_eliminate_loop
     16           0 LOAD_GLOBAL              0 (itertools)
                  3 LOAD_ATTR                1 (groupby)
     17           6 LOAD_FAST                1 (words)
                  9 LOAD_CONST               1 ('key')
     18          12 LOAD_GLOBAL              2 (operator)
                 15 LOAD_ATTR                3 (itemgetter)
                 18 LOAD_CONST               2 (0)
                 21 CALL_FUNCTION            1 (1 positional, 0
    keyword pair)
                 24 CALL_FUNCTION          257 (1 positional, 1
    keyword pair)
                 27 STORE_FAST               2 (grouped)
     21          30 LOAD_CONST               3 (<code object
    <dictcomp> at 0x101517930, file ".../dis_eliminate_loop.py",
line 21>)
                 33 LOAD_CONST               4
    ('Dictionary.load_data.<locals>.<dictcomp>')
                 36 MAKE_FUNCTION            0
     23          39 LOAD_FAST                2 (grouped)
                 42 GET_ITER
                 43 CALL_FUNCTION            1 (1 positional, 0
    keyword pair)
                 46 LOAD_FAST                0 (self)
                 49 STORE_ATTR               4 (by_letter)
                 52 LOAD_CONST               0 (None)
                 55 RETURN_VALUE
    TIME: 0.0332
编译器优化
反编译编译源还会暴露编译器所做的一些优化。 例如,在可能的情况下,在编译期间折叠文字表达式。
dis_constant_folding.py
#!/usr/bin/env python3
# encoding: utf-8
# Folded
i = 1 + 2
f = 3.4 * 5.6
s = 'Hello,' + ' World!'
# Not folded
I = i * 3 * 4
F = f / 2 / 3
S = s + '\n' + 'Fantastic!'
第 5-7 行的表达式中的值都不能改变执行操作的方式,因此表达式的结果可以在编译时计算并折叠成单个 LOAD_CONST 指令。 第 10-12 行不是这样。 因为变量涉及这些表达式,并且变量可能引用了重载所涉及的运算符的对象,所以执行必须延迟到运行时。
$ python3 -m dis dis_constant_folding.py
  5           0 LOAD_CONST              11 (3)
              2 STORE_NAME               0 (i)
  6           4 LOAD_CONST              12 (19.04)
              6 STORE_NAME               1 (f)
  7           8 LOAD_CONST              13 ('Hello, World!')
             10 STORE_NAME               2 (s)
 10          12 LOAD_NAME                0 (i)
             14 LOAD_CONST               6 (3)
             16 BINARY_MULTIPLY
             18 LOAD_CONST               7 (4)
             20 BINARY_MULTIPLY
             22 STORE_NAME               3 (I)
 11          24 LOAD_NAME                1 (f)
             26 LOAD_CONST               1 (2)
             28 BINARY_TRUE_DIVIDE
             30 LOAD_CONST               6 (3)
             32 BINARY_TRUE_DIVIDE
             34 STORE_NAME               4 (F)
 12          36 LOAD_NAME                2 (s)
             38 LOAD_CONST               8 ('\n')
             40 BINARY_ADD
             42 LOAD_CONST               9 ('Fantastic!')
             44 BINARY_ADD
             46 STORE_NAME               5 (S)
             48 LOAD_CONST              10 (None)
             50 RETURN_VALUE
另请参阅
- dis 标准库文档 -- 包括 字节码介绍 的列表。
Include/opcode.h-- CPython 解释器的源代码定义了opcode.h中的字节代码。- Python Essential Reference, 第四版, David M. Beazley -- www.informit.com/store/product.aspx...
- thomas.apestaart.org "Python Disassembly" -- 简要讨论在 Python 2.5 和2.6 之间存储字典中值的区别。
- Why is looping over range() in Python fast... -- StackOverflow 上关于通过反汇编的字节码比较 2 个循环示例的讨论。
- Decorator for binding constants at compile... -- Raymond Hettinger 和 Skip Montanaro 的 Python Cookbook 参考,说明了一个函数装饰器,它重写函数的字节码以插入全局常量,以避免运行时名称查找。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
 
           Python 3 标准库实例教程
Python 3 标准库实例教程 
             
             关于 LearnKu
                关于 LearnKu
               
                     
                     
                     粤公网安备 44030502004330号
 粤公网安备 44030502004330号