《Python高性能编程》——2.8 用line_profiler进行逐行分析

简介:

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

2.8 用line_profiler进行逐行分析

根据Ian的观点,Robert Kern的line_profiler是调查Python的CPU密集型性能问题最强大的工具。它可以对函数进行逐行分析,你应该先用cProfile找到需要分析的函数,然后用line_profiler对函数进行分析。

当你修改你的代码时,值得打印出这个工具的输出以及代码的版本,这样你就拥有一个代码变化(无论有没有用)的记录,让你可以随时查阅。当你在进行逐行改变时,不要依赖你的记忆。

输入命令pip install line_profiler来安装line_profiler。

用修饰器(@profile)标记选中的函数。用kernprof.py脚本运行你的代码,被选函数每一行花费的CPU时间以及其他信息就会被记录下来。

 备忘 

需要修改源代码这点比较讨厌,因为额外的修饰器会影响你的单元测试,除非你创建一个伪修饰器——见2.13节中的No-op的@profile修饰器。

运行时参数-l代表逐行分析而不是逐函数分析,-v用于显示输出。没有-v,你会得到一个.lprof的输出文件,回头你可以用line_profiler模块对其进行分析。例2-6中,我们会完整运行一遍我们的CPU密集型函数。

例2-6 运行kernprof逐行分析被修饰函数的CPU开销

$ kernprof.py -l -v julia1_lineprofiler.py
...
Wrote profile results to julia1_lineprofiler.py.lprof
Timer unit: 1e-06 s

File: julia1_lineprofiler.py
Function: calculate_z_serial_purepython at line 9
Total time: 100.81 s

Line #      Hits   Per Hit  % Time  Line Contents
==================================================
     9                              @profile
    10                              def calculate_z_serial_purepython(maxiter,
                                                              zs, cs):
11                                      """Calculate output list using
                                        Julia update rule"""
12             1    6870.0     0.0      output = [0] * len(zs)
13       1000001       0.8     0.8      for i in range(len(zs)):
14       1000000       0.8     0.8          n = 0
15       1000000       0.8     0.8          z = zs[i]
16       1000000       0.8     0.8          c = cs[i]
17      34219980       1.1    36.2          while abs(z) < 2 and n < maxiter:
18      33219980       1.0    32.6             z = z * z + c
19      33219980       0.8    27.2             n += 1
20       1000000       0.9     0.9          output[i] = n
21             1       4.0     0.0      return output

引入kernprof.py导致了额外的运行时间。本例的calculate_z_serial_purepython花费了100秒,远高于使用print语句的13秒和cProfile的19秒。获得的好处则是我们现在得到了一个函数内部每一行花费时间的分析结果。

%Time列最有用——我们可以看到36%的时间花在了while测试上。不过我们不知道是第一条语句(abs(z) < 2)还是第二条语句(n < maxiter)更花时间。循环内,我们可以看到更新z也颇花时间。甚至n += 1都很贵!每次循环时,Python的动态查询机制都在工作,即使每次循环中我们使用的变量都是同样的类型——在这一点上,编译和类型指定(第7章)可以给我们带来巨大的好处。创建output列表以及第20行上的更新相对整个while循环来说相当便宜。

对while语句更进一步的分析明显就是将两个判断拆开。Python社区中有一些讨论关于是否需要重写.pyc文件中对于一行语句中多个部分的具体信息,但目前还没有一个工具提供比line_profiler更细粒度的分析。

在例2-7中,我们将while语句分拆成多个语句。这一额外的复杂度会增加函数的运行时间,因为我们有了更多行代码需要执行,但它可能可以帮助我们了解这部分代码的开销。

 问题

在你看代码之前,你是否认为我们可以用这种方式了解基本操作的开销?其他因素会不会让分析变得更复杂?

例2-7 将组合式while语句拆成单个语句来记录每一部分的开销

$ kernprof.py -l -v julia1_lineprofiler2.py
...
Wrote profile results to julia1_lineprofiler2.py.lprof
Timer unit: 1e-06 s

File: julia1_lineprofiler2.py
Function: calculate_z_serial_purepython at line 9
Total time: 184.739 s

Line #     Hits    Per Hit  % Time  Line  Contents
===================================================
     9                              @profile
    10                              def calculate_z_serial_purepython(maxiter,
                                                               zs, cs):
    11                                 """Calculate output list using
                                       Julia update rule"""
    12         1   6831.0      0.0     output = [0] * len(zs)
    13   1000001      0.8      0.4     for i in range(len(zs)):
    14   1000000      0.8      0.4         n = 0
    15   1000000      0.9      0.5         z = zs[i]
    16   1000000      0.8      0.4         c = cs[i]
    17  34219980      0.8      14.9        while True:
    18  34219980      1.0      19.0            not_yet_escaped = abs(z) < 2
    19  34219980      0.8      15.5            iterations_left = n < maxiter
    20  34219980      0.8      15.1            if not_yet_escaped
                                                   and iterations_left:
    21   33219980     1.0      17.5                z = z * z + c
    22   33219980     0.9      15.3                n += 1
    23                                         else:
    24   1000000      0.8       0.4                break
    25   1000000      0.9       0.5            output[i] = n
    26         1      5.0       0.0        return output

这个版本花了184秒执行,而之前的仅100秒。其他因素确实让分析变得更复杂。本例中每一条额外语句都执行了34219980次,拖慢了代码。如果不是通过kernprof.py调查了每行的影响,我们可能会在缺乏证据的情况下得出是其他原因导致了变慢的结论。

此时有必要回到之前的timeit技术来测试每个单独表达式的开销:

>>> z = 0+0j # a point in the middle of our image
>>> %timeit abs(z) < 2 # tested inside IPython

10000000 loops, best of 3: 119 ns per loop

>>> n = 1
>>> maxiter = 300
>>> %timeit n < maxiter

10000000 loops, best of 3: 77 ns per loop

从这一简单分析上来看,对n的逻辑测试的速度几乎是abs函数调用的两倍。既然Python语句的评估次序是从左到右且支持短路,那么我们应该将最便宜的测试放在左边。每301次测试就有1次n < maxiter的值为False,这样Python就不必评估and操作符右边的语句了。

在评估前我们永远无法知道abs(z) < 2的值何时为False,而我们之前对复数平面的观察告诉我们300次迭代中大约10%的可能是True。如果我们想要更进一步了解这段代码的时间复杂度,有必要继续进行数值分析。不过在目前的情况下,我们只是想要看看有没有快速提高的机会。

我们可以做一个新的假设声明,“通过交换while语句的次序,我们会获得一个可靠的速度提升。”我们可以用kernprof.py测试这个假设,但是其额外的开销  可能会给我们的结果带来太多噪声。所以我们用一个之前版本的代码,测试比较while abs(z) < 2 and n < maxiter:和while n < maxiter and abs(z) < 2:之间的区别。

结果显示出大约0.4秒的稳定提升。这一结果显然很无足轻重且局限性太强,使用另一个更合适的方法(如换用第7章描述的Cython或PyPy)来解决问题会带来更高的收益。

我们对自己的结果有信心,是因为:

  • 我们声明的假设易于测试。
  • 我们对代码的改动仅局限于假设的测试(永远不要一次测试两件 事!)。
  • 我们收集了足够的证据支持我们的结论。

为了保持完整性,我们可以在包含了我们优化的两个主要函数上最后运行一次kernprof.py来确认我们代码整体的复杂度。例2-8交换了第17行while测试的语句,我们可以看到原来占用的36.1%的执行时间现在仅占用35.9%(这一结果在多次运行中稳定存在)。

例2-8 交换while语句的次序提升测试的速度

$ kernprof.py -l -v julia1_lineprofiler3.py
...
Wrote profile results to julia1_lineprofiler3.py.lprof
Timer unit: 1e-06 s

File: julia1_lineprofiler3.py
Function: calculate_z_serial_purepython at line 9
Total time: 99.7097 s

Line #     Hits   PerHit   %Time   Line Contents
==================================================
     9                             @profile
    10                             def calculate_z_serial_purepython(maxiter,
                                                                   zs, cs):
    11                                 """Calculate output list using
                                       Julia update rule"""
    12        1   6831.0     0.0       output = [0] * len(zs)
    13  1000001      0.8     0.8       for i in range(len(zs)):
    14  1000000      0.8     0.8           n = 0
    15  1000000      0.9     0.9           z = zs[i]
    16  1000000      0.8     0.8           c = cs[i]
    17 34219980      1.0    35.9           while n < maxiter and abs(z) < 2:
    18 33219980      1.0    32.0               z = z * z + c
    19 33219980      0.8    27.9               n += 1
    20  1000000      0.9    0.9            output[i] = n
    21    1          5.0    0.0        return output

和预期的一样,我们可以看例2-9的输出中,calculate_z_serial_purepython占用了其父函数97%的时间。创建列表的步骤相对来说无足轻重。

例2-9 逐行测试设置阶段的开销

File: julia1_lineprofiler3.py
Function: calc_pure_python at line 24
Total time: 195.218 s

Line #     Hits   Per Hit   % Time  Line Contents
=================================================
    24                              @profile
    25                              def calc_pure_python(draw_output,
                                                         desired_width,
                                                         max_iterations):
...
    44        1       1.0      0.0     zs = []
    45        1       1.0      0.0     cs = []
    46     1001       1.1      0.0     for ycoord in y:
    47  1001000       1.1      0.5         for xcoord in x:
    48  1000000       1.5      0.8             zs.append(
                                                      complex(xcoord, ycoord))
    49  1000000       1.6      0.8             cs.append(
                                                      complex(c_real, c_imag))
    50
    51        1      51.0      0.0      print "Length of x:", len(x)
    52        1      11.0      0.0      print "Total elements:", len(zs)
    53        1       6.0      0.0      start_time = time.time()
    54        1 191031307.0      97.9   output =
                                          calculate_z_serial_purepython
                                          (max_iterations, zs, cs)
    55        1       4.0      0.0      end_time = time.time()
    56        1       2.0      0.0      secs = end_time - start_time
    57        1      58.0      0.0      print calculate_z_serial_purepython
                                          .func_name + " took", secs, "seconds"
    58
                                     # this sum is expected for 1000^2 grid...
    59        1     9799.0     0.0   assert sum(output) == 33219980
相关文章
|
9天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
9天前
|
程序员 开发者 Python
Python网络编程基础(Socket编程) 错误处理和异常处理的最佳实践
【4月更文挑战第11天】在网络编程中,错误处理和异常管理不仅是为了程序的健壮性,也是为了提供清晰的用户反馈以及优雅的故障恢复。在前面的章节中,我们讨论了如何使用`try-except`语句来处理网络错误。现在,我们将深入探讨错误处理和异常处理的最佳实践。
|
7天前
|
机器学习/深度学习 人工智能 算法
图像处理与分析:Python中的计算机视觉应用
【4月更文挑战第12天】Python在计算机视觉领域广泛应用,得益于其丰富的库(如OpenCV、Pillow、Scikit-image)和跨平台特性。图像处理基本流程包括获取、预处理、特征提取、分类识别及重建生成。示例代码展示了面部和物体检测,以及使用GAN进行图像生成。
|
1天前
|
安全 数据处理 开发者
《Python 简易速速上手小册》第7章:高级 Python 编程(2024 最新版)
《Python 简易速速上手小册》第7章:高级 Python 编程(2024 最新版)
15 1
|
1天前
|
人工智能 数据挖掘 程序员
《Python 简易速速上手小册》第1章:Python 编程入门(2024 最新版)
《Python 简易速速上手小册》第1章:Python 编程入门(2024 最新版)
28 0
|
2天前
|
vr&ar Python
Python 用ARIMA、GARCH模型预测分析股票市场收益率时间序列4
Python 用ARIMA、GARCH模型预测分析股票市场收益率时间序列
23 0
|
2天前
|
机器学习/深度学习 算法 数据可视化
python用支持向量机回归(SVR)模型分析用电量预测电力消费
python用支持向量机回归(SVR)模型分析用电量预测电力消费
26 7
机器学习/深度学习 算法 Python
16 0
|
2天前
|
API Python
Python模块化编程:面试题深度解析
【4月更文挑战第14天】了解Python模块化编程对于构建大型项目至关重要,它涉及代码组织、复用和维护。本文深入探讨了模块、包、导入机制、命名空间和作用域等基础概念,并列举了面试中常见的模块导入混乱、不适当星号导入等问题,强调了避免循环依赖、合理使用`__init__.py`以及理解模块作用域的重要性。掌握这些知识将有助于在面试中自信应对模块化编程的相关挑战。
17 0
|
2天前
|
算法 数据可视化 Python
Python中LARS和Lasso回归之最小角算法Lars分析波士顿住房数据实例
Python中LARS和Lasso回归之最小角算法Lars分析波士顿住房数据实例
11 0