开发者社区> 异步社区> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

《Python高性能编程》——2.12 用dis模块检查CPython字节码

简介:
+关注继续查看

本节书摘来自异步社区《Python高性能编程》一书中的第2章,第2.12节,作者[美] 戈雷利克 (Micha Gorelick),胡世杰,徐旭彬 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.12 用dis模块检查CPython字节码

到目前为止我们已经展示了很多测量Python代码开销的方法(包括CPU和RAM的开销)。不过,我们还没有看到在底层虚拟机的字节码层面发生的事情。了解“台面下”发生的事情有助于在脑海中对运行慢的函数建立一个模型,并能帮助你编译你的代码。所以现在让我们来看一些字节码。

dis模块让我们能够看到基于栈的CPython虚拟机中运行的字节码。在你的Python代码运行的时候,了解虚拟机中发生了什么可以帮助你了解为什么某些编码风格会比其他的更快。同时还能帮助你使用Cython这样的工具,它跳出了Python的范畴,能够生成C代码。

dis模块是内建的。你可以传给它一段代码或者一个模块,它会打印出分解的字节码。在例2-16中我们分解了函数的外层循环。

 问题

你应该试着分解一个你自己的函数并将每一个分解的代码和分解的输出匹配起来。你能匹配下面的dis输出和原始函数吗?

例2-16 使用内建的dis模块来了解运行代码的虚拟机

In [1]: import dis
In [2]: import julia1_nopil

In [3]: dis.dis(julia1_nopil.calculate_z_serial_purepython)
 11           0 LOAD_CONST               1 (0)
              3 BUILD_LIST               1
              6 LOAD_GLOBAL              0 (len)
              9 LOAD_FAST                1 (zs)
             12 CALL_FUNCTION            1
             15 BINARY_MULTIPLY
             16 STORE_FAST               3 (output)

 12          19 SETUP_LOOP             123 (to 145)
             22 LOAD_GLOBAL              1 (range)
             25 LOAD_GLOBAL              0 (len)
             28 LOAD_FAST                1 (zs)
             31 CALL_FUNCTION            1
             34 CALL_FUNCTION            1
             37 GET_ITER
          >> 38 FOR_ITER               103 (to 144)
             41 STORE_FAST               4 (i)

 13          44 LOAD_CONST               1 (0)
             47 STORE_FAST               5 (n)
# ...
# We'll snip the rest of the inner loop for brevity!
# ...

 19     >> 131 LOAD_FAST                 5 (n)
           134 LOAD_FAST                 3 (output)
           137 LOAD_FAST                 4 (i)
           140 STORE_SUBSCR
           141 JUMP_ABSOLUTE            38
        >> 144 POP_BLOCK

 20     >> 145 LOAD_FAST                 3 (output)
           148 RETURN_VALUE

这个输出非常的直白简明。第一列包含了原始文件的行数。第二列包含了一些>>标志,它们是指向其他代码的跳转点。第三列是操作的地址和操作名。第四列包含了操作的参数。第五列的标记可用来帮助对照字节码和原始Python的参数。

对照字节码和例2-3中的Python代码。字节码首先将常数0放到栈上,然后创建了一个具有一个项目的列表。接下来,它搜索了名字空间来查询len函数,将它放到栈上,再次搜索名字空间找到zs,放到栈上。在第12行,它从栈上调用len函数,且弹出了栈上的zs作为参数;然后对最后两个参数调用二进制乘法(zs的长度和那个单项目列表)将结果保存在output中。这就是我们那个Python函数的第一行干的事情。你可以继续查看下一段字节码来了解Python代码第二行的行为(外层for循环)。

 问题 

跳转点(>>)匹配JUMP_ABSOLUTE以及POP_JUMP_IF_FALSE等指令。过一遍你自己的函数分解结果并对照跳转点和跳转指令。

介绍完字节码,我们现在要问:要完成同样的任务,显式编写的函数和使用内建函数在字节码和时间开销上的对比是什么。

不同的方法,不同的复杂度

应该有一种——而且最好只有唯一的一种——明显的方式去完成它。虽然这种方式可能一开始并不明显,除非你是荷兰人……

——Tim Peters

Python之禅

通过Python你有无数种方式表达你的意思。一般来说最优的那个选择十分明显,但是如果你的经验主要来自一个老版本的Python或另一门编程语言,那么在你的脑海里可能就是另外的选择。某些表达的方式可能就比别的要慢。

对于你大多数的代码,你可能更关心可读性而不是速度,这能让你的团队更有效地写代码,而不是被高效而难懂的代码所迷惑。但是,有些时候你会更追求性能(且不牺牲可读性),那么你需要的可能是一些速度测试。

看看例2-17的两段代码。它们都做了相同的工作,但是第一个会产生大量额外的Python字节码,带来更大的开销。

例2-17 一个单纯的和一个高效的手段解决同一个求和问题

def fn_expressive(upper = 1000000):
    total = 0
    for n in xrange(upper):
        total += n
    return total

def fn_terse(upper = 1000000):
    return sum(xrange(upper))

print "Functions return the same result:", fn_expressive() == fn_terse()

Functions return the same result:
True

两个函数都对一批整数求和。一个简单的经验法则(但是你一定要进行性能分析!)是字节码越多执行的速度越慢。内建函数使用了更少的字节码行数来完成同样的工作。我们在例2-18中使用IPython的%timeit魔法函数测量它们的最快运行时间。

例2-18 用%timeit验证我们的假设:内建函数应该比自己写函数要快(译注:原著中未给出fn_terse()的结果)

%timeit fn_expressive()

10 loops, best of 3: 42 ms per loop
100 loops, best of 3: 12.3 ms per loop

%timeit fn_terse()

如果我们用dis模块调查每个函数的字节码,如例2-19所示,我们能看到虚拟机用了17行来执行更有表现力的函数,而仅用了6行来执行非常可读但更简洁的第二个函数。

例2-19 用dis查看两个函数的字节码指令行数

import dis
print fn_expressive.func_name
dis.dis(fn_expressive)

fn_expressive
  2            0 LOAD_CONST             1 (0)
               3 STORE_FAST             1 (total)

  3            6 SETUP_LOOP            30 (to 39)
               9 LOAD_GLOBAL            0 (xrange)
              12 LOAD_FAST              0 (upper)
              15 CALL_FUNCTION          1
              18 GET_ITER
           >> 19 FOR_ITER              16 (to 38)
              22 STORE_FAST             2 (n)

  4           25 LOAD_FAST              1 (total)
              28 LOAD_FAST              2 (n)
              31 INPLACE_ADD
              32 STORE_FAST             1 (total)
              35 JUMP_ABSOLUTE         19
           >> 38 POP_BLOCK

  5        >> 39 LOAD_FAST              1 (total)
              42 RETURN_VALUE

print fn_terse.func_name
dis.dis(fn_terse)

fn_terse
  8            0 LOAD_GLOBAL            0 (sum)
               3 LOAD_GLOBAL            1 (xrange)
               6 LOAD_FAST              0 (upper)
               9 CALL_FUNCTION          1
              12 CALL_FUNCTION          1
              15 RETURN_VALUE

两段代码的区别很明显。在fn_expressive()内部,我们维护了两个本地变量并用for循环遍历了一个列表。for循环会在每次循环时检查StopIteration异常是否被引发。每次迭代都会调用total.__add__函数,这个函数会检查第二个变量(n)的类型。所有这些检查都会带来一些开销。

在fn_terse()内部,我们调用了一个用C编写的优化的列表操作函数,它知道如何生成最后的结果而无须创建中间的Python对象。这样会快很多,即使每次迭代仍然必须检查被求和对象的类型(我们会在第4章看到一些将类型固定的方法,这样就不需要在每次迭代时都检查它)。

之前提过,你一定要对你的代码进行性能分析——但如果你只依靠性能分析来试错,那你必然会在某些时候写出较慢的代码。学习Python是否已经存在一个内建的更短且依然可读的方式来解决你的问题是绝对值得的。如果已经存在,那么它可能更容易被另一个开发人员理解且可能运行得会更快。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Python的GUI编程
Python的GUI编程 使用Tkinter模块来创建简单的GUI程序。Tkinter的Widgets有:Button、Canvas、Checkbutton、Entry、Frame、Label、Listbox、Menu、Menubutton、Message、Radiobutton、Scales、Scrollbar、TEXT、Toplevel等。
777 0
使用python进行编程.工具是次要的.关键在理解代码.
开始研究下python.还是蛮喜欢这个语言的. 很简单.很有意思. 也用过eclipse的python插件. 也用过 nodepad ++ 进行编辑. 最后发现.其实python自带的
841 0
Python编程-基础知识-字符串格式化
1. 简单字符串格式化   format = "Hello, how are you doing %s %s!"values = ("David", "Gu")print format % values   Result: Hello, how are you doing David Gu!   format = "PI with seven decimals: %.
593 0
Python编程-基础知识-列表和元组
列表示例1:  (新建, 删除, 修改) # basic operation of listnames = ['David', 'George', 'Peter', 'Mark', 'ALice']print "Original List:"print namesdel names[1]p...
730 0
python 教程 第十八章、 Web编程
第十八章、 Web编程 import urllib2 LOGIN = 'jin' PASSWD = 'Welcome' URL = 'https://tlv-tools-qc:8443/qcbin/start_a.
875 0
python 教程 第十九章、 图形界面编程
第十九章、 图形界面编程 import Tkinter top = Tkinter.Tk() hello = Tkinter.Label(top, text='Hello World!') hello.
782 0
Python专家编程
版权声明:本文为博主chszs的原创文章,未经博主允许不得转载。 https://blog.csdn.net/chszs/article/details/3767771 Python专家编程 一、CPythonCPython是一个默认的、广泛使用的Python编程语言的实现。
771 0
Python的GUI编程
版权声明:本文为博主chszs的原创文章,未经博主允许不得转载。 https://blog.csdn.net/chszs/article/details/3729155 Python的GUI编程 使用Tkinter模块来创建简单的GUI程序。
764 0
+关注
异步社区
异步社区(www.epubit.com)是人民邮电出版社旗下IT专业图书旗舰社区,也是国内领先的IT专业图书社区,致力于优质学习内容的出版和分享,实现了纸书电子书的同步上架,于2015年8月上线运营。公众号【异步图书】,每日赠送异步新书。
文章
问答
文章排行榜
最热
最新
相关电子书
更多
Python系列直播第一讲——Python中的一切皆对象
立即下载
Python第五讲——关于爬虫如何做js逆向的思路
立即下载
Python 脚本速查手册
立即下载