《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是否已经存在一个内建的更短且依然可读的方式来解决你的问题是绝对值得的。如果已经存在,那么它可能更容易被另一个开发人员理解且可能运行得会更快。

相关文章
|
10天前
|
Python
Python Internet 模块
Python Internet 模块。
105 74
|
28天前
|
算法 数据安全/隐私保护 开发者
马特赛特旋转算法:Python的随机模块背后的力量
马特赛特旋转算法是Python `random`模块的核心,由松本真和西村拓士于1997年提出。它基于线性反馈移位寄存器,具有超长周期和高维均匀性,适用于模拟、密码学等领域。Python中通过设置种子值初始化状态数组,经状态更新和输出提取生成随机数,代码简单高效。
105 63
|
1月前
|
测试技术 Python
手动解决Python模块和包依赖冲突的具体步骤是什么?
需要注意的是,手动解决依赖冲突可能需要一定的时间和经验,并且需要谨慎操作,避免引入新的问题。在实际操作中,还可以结合使用其他方法,如虚拟环境等,来更好地管理和解决依赖冲突😉。
|
1月前
|
持续交付 Python
如何在Python中自动解决模块和包的依赖冲突?
完全自动解决所有依赖冲突可能并不总是可行,特别是在复杂的项目中。有时候仍然需要人工干预和判断。自动解决的方法主要是提供辅助和便捷,但不能完全替代人工的分析和决策😉。
|
1月前
|
数据可视化 Python
如何在Python中解决模块和包的依赖冲突?
解决模块和包的依赖冲突需要综合运用多种方法,并且需要团队成员的共同努力和协作。通过合理的管理和解决冲突,可以提高项目的稳定性和可扩展性
|
21天前
|
人工智能 数据可视化 数据挖掘
探索Python编程:从基础到高级
在这篇文章中,我们将一起深入探索Python编程的世界。无论你是初学者还是有经验的程序员,都可以从中获得新的知识和技能。我们将从Python的基础语法开始,然后逐步过渡到更复杂的主题,如面向对象编程、异常处理和模块使用。最后,我们将通过一些实际的代码示例,来展示如何应用这些知识解决实际问题。让我们一起开启Python编程的旅程吧!
|
20天前
|
存储 数据采集 人工智能
Python编程入门:从零基础到实战应用
本文是一篇面向初学者的Python编程教程,旨在帮助读者从零开始学习Python编程语言。文章首先介绍了Python的基本概念和特点,然后通过一个简单的例子展示了如何编写Python代码。接下来,文章详细介绍了Python的数据类型、变量、运算符、控制结构、函数等基本语法知识。最后,文章通过一个实战项目——制作一个简单的计算器程序,帮助读者巩固所学知识并提高编程技能。
|
8天前
|
Unix Linux 程序员
[oeasy]python053_学编程为什么从hello_world_开始
视频介绍了“Hello World”程序的由来及其在编程中的重要性。从贝尔实验室诞生的Unix系统和C语言说起,讲述了“Hello World”作为经典示例的起源和流传过程。文章还探讨了C语言对其他编程语言的影响,以及它在系统编程中的地位。最后总结了“Hello World”、print、小括号和双引号等编程概念的来源。
101 80
|
27天前
|
存储 索引 Python
Python编程数据结构的深入理解
深入理解 Python 中的数据结构是提高编程能力的重要途径。通过合理选择和使用数据结构,可以提高程序的效率和质量
134 59
|
7天前
|
分布式计算 大数据 数据处理
技术评测:MaxCompute MaxFrame——阿里云自研分布式计算框架的Python编程接口
随着大数据和人工智能技术的发展,数据处理的需求日益增长。阿里云推出的MaxCompute MaxFrame(简称“MaxFrame”)是一个专为Python开发者设计的分布式计算框架,它不仅支持Python编程接口,还能直接利用MaxCompute的云原生大数据计算资源和服务。本文将通过一系列最佳实践测评,探讨MaxFrame在分布式Pandas处理以及大语言模型数据处理场景中的表现,并分析其在实际工作中的应用潜力。
34 2