《Python高性能编程》——2.9 用memory_profiler诊断内存的用量

2.9 用memory_profiler诊断内存的用量

和Rober Kern实现的line_profiler包测量CPU占用率类似,Fabian Pedregosa和Philippe Gervais实现的memory_profiler模块能够逐行测量内存占用率。了解代码的内存使用情况允许你问自己两个问题:

  • 我们能不能重写这个函数让它使用更少的RAM来工作得更有效率?
  • 我们能不能使用更多RAM缓存来节省CPU周期?

memory_profiler的操作和line_profiler十分类似,但是运行速度要慢的多。如果你安装了psutil包(强烈推荐),memory_profiler会跑得快一点。内存分析可以轻易让你的代码慢上10到100倍。所以实际操作时你可能只是偶尔使用memory_profiler而更多地使用line_profiler来进行CPU分析。

用命令pip install memory_profiler来安装memory_profiler(可选装pip install psutil)。

之前说过,memory_profiler的实现可能并不如line_profiler那么有效率。所以你最好将测试局限在一个较小的问题上,这样才能在一个可容忍的时间内结束。最终验证可以用一整夜来跑,但你需要一个迅速而合理的迭代周期用来分析问题并验证假设。例2-10的代码使用了完整的1000×1000网格,在Ian的笔记本电脑上花了1.5个小时来收集数据。

 备忘 

需要修改源代码这点比较讨厌。和line_profiler一样,修饰器(@profile)被用来标记选中的函数。这会影响你的单元测试,除非你创建一个伪修饰器——见第57页No-op的@profile修饰器。

在处理内存分配时,你必须意识到情况不像CPU占用率那么直截了当。通常让一个进程将内存超额分配给本地内存池并在空闲时使用会更有效率,因为内存分配操作非常昂贵。另外,垃圾收集不会立即进行,所以对象可能在被销毁后依然存在于垃圾收集池中一段时间。

使用这些技术的后果就是很难真正了解一个Python程序内部的内存使用和释放的情况,因为当从进程外部观察时,某一行代码可能不会分配固定数量的内存。观察多行代码的内存占用趋势可能比只观察一行代码更具有洞察力。

让我们看看memory_profiler在例2-10中的输出。在第12行的calculate_z_serial_purepython中,我们看到分配1000000个项目导致大约7MB的RAM[1]被加入这个进程。这不意味着output列表的大小就是7MB,只是进程在列表内部分配时增长了大约7MB。第13行,我们看到进程在循环内又增长了32MB。这可能是由于调用了range。(例11-1进一步讨论了内存追溯,7MB和32MB的区别源于两个列表的内容。)在第46行的父进程中,我们看到zs和cs列表的分配占用了大约79MB。再强调一遍,这个数字不一定是数组的真实大小,只是进程在创建这些列表的过程中增长的大小。

例2-10 memory_profiler对我们两个主要函数的分析结果,在calculate_z_serial_purepython上显示出预料外的内存使用

$ python -m memory_profiler julia1_memoryprofiler.py
...
Line#    Mem usage   Increment   Line Contents
================================================
     9   89.934 MiB   0.000 MiB   @profile
    10                            def calculate_z_serial_purepython(maxiter,
                                                                  zs, cs):

    11                                """Calculate output list using...
    12   97.566 MiB    7.633 MiB       output = [0] * len(zs)
    13  130.215 MiB   32.648 MiB       for i in range(len(zs)):
    14  130.215 MiB    0.000 MiB           n = 0
    15  130.215 MiB    0.000 MiB           z = zs[i]
    16  130.215 MiB    0.000 MiB           c = cs[i]
    17  130.215 MiB    0.000 MiB           while n < maxiter and abs(z) < 2:
    18  130.215 MiB    0.000 MiB               z = z * z + c
    19  130.215 MiB    0.000 MiB               n += 1
    20  130.215 MiB    0.000 MiB           output[i] = n
    21  122.582 MiB   -7.633 MiB       return output

Line #     Mem usage   Increment   Line Contents
================================================
    24   10.574 MiB -112.008 MiB   @profile
    25                              def calc_pure_python(draw_output,
                                                    desired_width,
                                                    max_iterations):
    26                                 """Create a list of complex ...
    27   10.574 MiB    0.000 MiB       x_step = (float(x2 - x1) / ...
    28   10.574 MiB    0.000 MiB       y_step = (float(y1 - y2) / ...
    29   10.574 MiB    0.000 MiB       x = []
    30   10.574 MiB    0.000 MiB       y = []
    31   10.574 MiB    0.000 MiB       ycoord = y2
    32   10.574 MiB    0.000 MiB       while ycoord > y1:
    33   10.574 MiB    0.000 MiB           y.append(ycoord)
    34   10.574 MiB    0.000 MiB           ycoord += y_step
    35   10.574 MiB    0.000 MiB       xcoord = x1
    36   10.582 MiB    0.008 MiB       while xcoord < x2:
    37   10.582 MiB    0.000 MiB           x.append(xcoord)
    38   10.582 MiB    0.000 MiB           xcoord += x_step
    ...
    44   10.582 MiB    0.000 MiB       zs = []
    45   10.582 MiB    0.000 MiB       cs = []
    46   89.926 MiB   79.344 MiB       for ycoord in y:
    47   89.926 MiB    0.000 MiB          for xcoord in x:
    48   89.926 MiB    0.000 MiB              zs.append(complex(xcoord, ycoord))
    49   89.926 MiB    0.000 MiB              cs.append(complex(c_real, c_imag))
    50
    51   89.934 MiB    0.008 MiB        print "Length of x:", len(x)
    52   89.934 MiB    0.000 MiB        print "Total elements:", len(zs)
    53   89.934 MiB    0.000 MiB        start_time = time.time()
    54                                  output = calculate_z_serial...
    55  122.582 MiB   32.648 MiB        end_time = time.time()
    ...

另一种展示内存使用变化的方式是随时间采样并画图。memory_profiler有一个功能叫mprof,用于对内存使用情况进行采样和画图。它的采样基于时间而不是代码行,因而不会影响代码的运行时间。

图2-6是mprof运行julia1_memoryprofiler.py生成的。它会首先生成一个统计文件,然后再用mprof画图。图中展示了我们的两个主要函数的执行开始时间以及运行时RAM的增长情况。在calculate_z_serial_purepython内,我们可以看到RAM在函数的整个执行时间内都平稳增长,这是为了生成那些小对象(int和float类型)。

除了在函数层面上观察行为以外,我们还可以使用环境管理器添加标签。例2-11的代码用于生成图2-7。我们可以看到create_output_list标签:它在calculate_z_serial_purepython之后出现,并导致进程分配更多RAM。然后,为了让图更便于理解,我们用time.sleep(1)暂停1秒。

在create_range_of_zs标签之后,我们看到RAM的使用出现了一个猛增;在例2-11的代码中,你可以看到这个标签出现在创建iterations列表时。我们创建列表用的是range而不是xrange——图显示的很清楚,为了创建一个索引,一个具有1 000 000个项目的大列表被实例化。这个方法很没有效率,一旦遇到更大的列表,扩展性也不好(我们的RAM会耗尽!)。用于创建这个列表的内存分配操作本身也占用了一点时间,却没有为这个函数带来什么有用的贡献。

 备忘 

在Python 3中range的行为改变了,它跟Python 2的xrange一样。xrange在Python 3中已被淘汰,2to3转换工具会自动帮你进行这一转换。

例2-11 用环境管理器给mprof图像添加标签

@profile
def calculate_z_serial_purepython(maxiter, zs, cs):
    """Calculate output list using Julia update rule"""
    with profile.timestamp("create_output_list"):
        output = [0] * len(zs)
    time.sleep(1)
    with profile.timestamp("create_range_of_zs"):
        iterations = range(len(zs))
        with profile.timestamp("calculate_output"):
            for i in iterations:
                n = 0
                z = zs[i]
                c = cs[i]
                while n < maxiter and abs(z) < 2:
                    z = z * z + c
                    n += 1
                output[i] = n
    return output

对于占据了图像大部分时间的calculate_output,我们可以看到RAM有一个非常缓慢的线性增长,这是用于分配给内部循环中所有用到的临时数字。使用标签确实可以帮助我们细粒度地了解内存的使用情况。

最后,我们可将range调用替换成xrange。在图2-8中,我们可以看到内部循环的RAM使用情况相应降低了。

如果我们想要测量某些语句的RAM使用情况,我们可以用IPython的%memit魔法函数,其工作方式类似于%timeit。第11章会详细讨论%memit的用法以及各种增进RAM使用效率的方法。

时间: 2024-08-31 02:02:09

《Python高性能编程》——2.9 用memory_profiler诊断内存的用量的相关文章

《Python高性能编程》——导读

前 言 Python很容易学.你之所以阅读本书可能是因为你的代码现在能够正确运行,而你希望它能跑得更快.你可以很轻松地修改代码,反复地实现你的想法,你对这一点很满意.但能够轻松实现和代码跑得够快之间的取舍却是一个世人皆知且令人惋惜的现象.而这个问题其实是可以解决的. 有些人想要让顺序执行的过程跑得更快.有些人需要利用多核架构.集群,或者图形处理单元的优势来解决他们的问题.有些人需要可伸缩系统在保证可靠性的前提下酌情或根据资金多少处理更多或更少的工作.有些人意识到他们的编程技巧,通常是来自其他语言

《Python高性能编程》——1.2 将基本的元素组装到一起

1.2 将基本的元素组装到一起 仅理解计算机的基本组成部分并不足以理解高性能编程的问题.所有这些组件的互动与合作还会引入新的复杂度.本段将研究一些样本问题,描述理想的解决方案以及Python如何实现它们. 警告:本段可能看上去让人绝望--大多数问题似乎都证明Python并不适合解决性能问题.这不是真的,原因有两点.首先,在所有这些"高性能计算要素"中,我们忽视了一个至关重要的要素:开发者.原生Python在性能上欠缺的功能会被迅速开发出来.另外,我们会在本书中介绍各种模块和原理来帮助减

《Python高性能编程》——第1章 理解高性能Python 1.1 基本的计算机系统

第1章 理解高性能Python 读完本章之后你将能够回答下列问题 计算机架构有哪些元素? 常见的计算机架构有哪些? 计算机架构在Python中的抽象表达是什么? 实现高性能Python代码的障碍在哪里? 性能问题有哪些种类? 计算机编程可以被认为是以特定的方式进行数据的移动和转换来得到某种结果.然而这些操作有时间上的开销.因此,高性能编程可以被认为是通过降低开销(比如撰写更高效的代码)或改变操作方式(比如寻找一种更合适的算法)来让这些操作的代价最小化. 数据的移动发生在实际的硬件上,我们可以通过

《Python高性能编程》——1.3 为什么使用Python

1.3 为什么使用Python Python具有高度的表现力且容易上手--新开发者会很快发现他们可以在很短时间里做到很多.许多Python库包含了用其他语言编写的工具,使Python可以轻易调用其他系统.比如,scikit-learn机器学习系统包含了LIBLINEAR和LIBSVM(两者皆以C语言写成),numpy库则包含了BLAS以及其他用C和Fortran语言写的库.因此,正确运用这些库的Python代码确实可以在速度上做到跟C媲美. Python被誉为"内含电池",因为它内建了

《Python高性能编程》——2.6 使用cProfile模块

2.6 使用cProfile模块 cProfile是一个标准库内建的分析工具.它钩入CPython的虚拟机来测量其每一个函数运行所花费的时间.这一技术会引入一个巨大的开销,但你会获得更多的信息.有时这些额外的信息会给你的代码带来令人惊讶的发现. cProfile是标准库内建的三个分析工具之一,另外两个是hotshot和profile.hotshot还处于实验阶段,profile则是原始的纯Python分析器.cProfile具有跟profile一样的接口,且是默认的分析工具.如果你对这些库的历史

《Python高性能编程》——第2章 通过性能分析找到瓶颈 2.1 高效地分析性能

第2章 通过性能分析找到瓶颈 读完本章之后你将能够回答下列问题 如何找到代码中速度和RAM的瓶颈? 如何分析CPU和内存使用情况? 我应该分析到什么深度? 如何分析一个长期运行的应用程序? 在CPython台面下发生了什么? 如何在调整性能的同时确保功能的正确? 性能分析帮助我们找到瓶颈,让我们在性能调优方面做到事半功倍.性能调优包括在速度上巨大的提升以及减少资源的占用,也就是说让你的代码能够跑得"足够快"以及"足够瘦".性能分析能够让你用最小的代价做出最实用的决定

《Python高性能编程》——2.13 在优化期间进行单元测试保持代码的正确性

2.13 在优化期间进行单元测试保持代码的正确性 如果你不对你的代码进行单元测试,那么从长远来看你可能正在损害你的生产力.Ian(脸红)十分尴尬地提到有一次他花了一整天的时间优化他的代码,因为嫌麻烦所以他禁用了单元测试,最后却发现那个显著的速度提升只是因为他破坏了需要优化的那段算法.这样的错误你一次都不要犯. 除了单元测试,你还应该坚定地考虑使用coverage.py.它会检查有哪些代码行被你的测试所覆盖并找出那些没有被覆盖的代码.这可以让你迅速知道你是否测试了你想要优化的代码,那么在优化过程中

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

2.8 用line_profiler进行逐行分析 根据Ian的观点,Robert Kern的line_profiler是调查Python的CPU密集型性能问题最强大的工具.它可以对函数进行逐行分析,你应该先用cProfile找到需要分析的函数,然后用line_profiler对函数进行分析. 当你修改你的代码时,值得打印出这个工具的输出以及代码的版本,这样你就拥有一个代码变化(无论有没有用)的记录,让你可以随时查阅.当你在进行逐行改变时,不要依赖你的记忆. 输入命令pip install lin

《Python高性能编程》——2.3 计算完整的Julia集合

2.3 计算完整的Julia集合 我们在本节分解Julia集合的生成代码.我们将在本章以各种方法分析它.如例2-1所示,在模块的一开始,我们导入time模块作为我们的第一种分析手段并定义一些坐标常量. 例2-1 定义空间坐标的全局常量 """Julia set generator without optional PIL-based image drawing""" import time # area of complex space to i