IPython功能(7)Profiling和Timing代码

为了提高编码效率,有时候需要检查单个或一系列代码的运行时间,这时候就可以用的IPython的一些相关命令,
下面我们会来讨论以下命令:
%time: 单个指令的运行时间
%timeit: 重复执行单个指令以获得更准确的时间
%prun: 运行代码并给出分析
%lprun: 运行代码并给出逐行分析
%memit: 单个指令的内存分析
%mprun: 运行代码的内存分析

最后4个不是IPython自带的,需要安装line_profiler和memory_profiler这2个扩展包。

代码片段计时:%timeit和%time

%timeit是行magic函数,%%timeit是单元magic函数,可以在IPython Magic Commands 查看函数功能,它们可以计算重复执行代码的时间。

%timeit sum(range(100))
100000 loops, best of 3: 1.54 µs per loop

请注意,由于此操作非常快,%timeit 会自动进行大量重复。
对于较慢的命令,%timeit 会自动调整并执行更少的重复:

%%timeit
total = 0
for i in range(1000):
    for j in range(1000):
        total += i * (-1) ** j
1 loops, best of 3: 407 ms per loop

有时重复不是最好的选择,我们可能会被重复操作误导,对预先排序进行排序比对未排序列表进行排序要快得多,因此重复会使结果发生偏差:

import random
L = [random.random() for i in range(100000)]
%timeit L.sort()
100 loops, best of 3: 1.9 ms per loop

这种情况用%time函数比较合适,长代码也同样适用,因为此时短的,系统性的延迟不会对结果产生影响。
我们先来看一下对预先排序进行排序比对未排序列表进行排序的时间:

import random
L = [random.random() for i in range(100000)]
print("sorting an unsorted list:")
%time L.sort()
sorting an unsorted list:
CPU times: user 40.6 ms, sys: 896 µs, total: 41.5 ms
Wall time: 41.5 ms
print("sorting an already sorted list:")
%time L.sort()
sorting an already sorted list:
CPU times: user 8.18 ms, sys: 10 µs, total: 8.19 ms
Wall time: 8.24 ms

可以看到预先排序的列表排起来很快,也可以看到 %time 花的时间比 %timeit 多很多,甚至对于预先排序的列表。因为%timeit在后台做了一些聪明的事情来防止系统调用干扰计时。

对于 %time 和 %timeit,双百分号可以对多行代码进行计时:

%%time
total = 0
for i in range(1000):
    for j in range(1000):
        total += i * (-1) ** j
CPU times: user 504 ms, sys: 979 µs, total: 505 ms
Wall time: 505 ms

了解%time 和 %timeit 的更多信息可以看下帮助文档,比如在IPython输入%time?。

分析整个脚本:%prun

一个程序由许多单个语句组成,有时在上下文中对这些语句进行计时比单独对它们计时更重要。Python包含一个内置代码分析器(可以在Python文档中阅读),但IPython提供了一种更方便的方法来使用此分析器,就是使用magic函数 %prun。

为了举例,我们先定义一个可以进行简单计算的函数:

def sum_of_lists(N):
    total = 0
    for i in range(5):
        L = [j ^ (j >> i) for j in range(N)]
        total += sum(L)
    return total

可以用%prun看下分析结果:

%prun sum_of_lists(1000000)

如果是在notebook里,结果会输出在页面里,长得像这样:

14 function calls in 0.714 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        5    0.599    0.120    0.599    0.120 <ipython-input-19>:4(<listcomp>)
        5    0.064    0.013    0.064    0.013 {built-in method sum}
        1    0.036    0.036    0.699    0.699 <ipython-input-19>:1(sum_of_lists)
        1    0.014    0.014    0.714    0.714 <string>:1(<module>)
        1    0.000    0.000    0.714    0.714 {built-in method exec}

结果是一个表,该表按每次函数调用的总时间顺序指示执行花费最多时间的位置。在这种情况下,大部分执行时间都在sum_of_lists 中的列表理解中。从这里开始,我们可以开始考虑可以进行哪些更改来提高算法性能。

%prun? 查看更多帮助信息。

逐行分析:%lprun

%prun 分析逐个函数的时候很有用,但有时逐行分析更方便,但这个功能不是内置在Python或者IPython中,可以安装一个叫line_profiler的包执行此操作。首先用Python的打包工具pip安装此包:

$ pip install line_profiler

然后可以用IPython加载该包:

%load_ext line_profiler

现在可以用%lprun功能进行逐行分析:

%lprun -f sum_of_lists sum_of_lists(5000)

输出结果类似以下:

Timer unit: 1e-06 s

Total time: 0.009382 s
File: <ipython-input-19-fa2be176cc3e>
Function: sum_of_lists at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def sum_of_lists(N):
     2         1            2      2.0      0.0      total = 0
     3         6            8      1.3      0.1      for i in range(5):
     4         5         9001   1800.2     95.9          L = [j ^ (j >> i) for j in range(N)]
     5         5          371     74.2      4.0          total += sum(L)
     6         1            0      0.0      0.0      return total

从以上结果中我们可以看到哪步程序花费的时间最多,以此为依据改进脚本。

同样%lprun? 查看帮助文档。

存储分析 %memit 和 %mprun

同样memory_profiler需要从外部安装:

$ pip install memory_profiler

IPython加载此包:

%load_ext memory_profiler

memory_profiler包含2个有用的magic函数:%memit 函数(提供了一个相当于 %timeit 的内存分析)和 %mprun 函数(提供了一个相当于 %lprun 的内存分析)。
%memit 用起来比较简单:

%memit sum_of_lists(1000000)
peak memory: 100.08 MiB, increment: 61.36 MiB

逐行代码的内存占用描述可以用 %mprun,但是可惜的是这个magic命令只能对单独模块里定义的function起作用(不能直接应用于notebook)。所以我们先用%%file函数定义一个简单的模块叫做mprun_demo.py,这个脚本里包含我们定义的sum_of_lists功能,
先写一下我们的mprun_demo.py脚本:

%%file mprun_demo.py
def sum_of_lists(N):
    total = 0
    for i in range(5):
        L = [j ^ (j >> i) for j in range(N)]
        total += sum(L)
        del L # remove reference to L
    return total

然后导入此功能并运行内存分析器:

from mprun_demo import sum_of_lists
%mprun -f sum_of_lists sum_of_lists(1000000)

输出的结果看起来像这样:

Filename: ./mprun_demo.py

Line #    Mem usage    Increment   Line Contents
================================================
     4     71.9 MiB      0.0 MiB           L = [j ^ (j >> i) for j in range(N)]


Filename: ./mprun_demo.py

Line #    Mem usage    Increment   Line Contents
================================================
     1     39.0 MiB      0.0 MiB   def sum_of_lists(N):
     2     39.0 MiB      0.0 MiB       total = 0
     3     46.5 MiB      7.5 MiB       for i in range(5):
     4     71.9 MiB     25.4 MiB           L = [j ^ (j >> i) for j in range(N)]
     5     71.9 MiB      0.0 MiB           total += sum(L)
     6     46.5 MiB    -25.4 MiB           del L # remove reference to L
     7     39.1 MiB     -7.4 MiB       return total

Increment这一列告诉我们每一行对总内存的影响:可以看到我们创建和删除L列表,增加和25MB的内存使用。最上面那行是Python编译器自己占用的内存。

同样可以用 %memit? 和%mprun? 查看更多信息。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容