如何逐行分析Python代码?

174

我一直在使用cProfile来对我的代码进行性能分析,效果非常好。我还使用gprof2dot.py来可视化结果(可以更清晰地展示)。

然而,cProfile(以及我目前看到的大多数其他Python性能分析器)似乎只能在函数调用级别上进行性能分析。当某些函数从不同的位置进行调用时,这会导致混淆——我不知道第一次调用还是第二次调用占用了大部分时间。当需要调用六个级别深的函数,并从七个其他地方调用时,情况变得更糟。

我该如何获得逐行性能分析的结果?

而不是像这样:

function #12, total time: 2.0s

我希望看到像这样的东西:

function #12 (called from somefile.py:102) 0.5s
function #12 (called from main.py:12) 1.5s

cProfile可以显示总时间中有多少“转移”到父级,但是当你有许多层和相互关联的调用时,这种连接再次丢失。

理想情况下,我希望有一个GUI可以解析数据,然后显示我的源文件以及每行所需的总时间。类似于这样:

main.py:

a = 1 # 0.0s
result = func(a) # 0.4s
c = 1000 # 0.0s
result = func(c) # 5.0s

那么我将能够点击第二个“func(c)”调用,以查看在该调用中占用时间的内容,而与“func(a)”调用分开。这样说您明白了吗?


2
我猜你会对 pstats.print_callers 感兴趣。这里有一个例子:http://www.doughellmann.com/PyMOTW/profile/。 - Muhammad Alkarouri
5个回答

152

我相信这就是Robert Kern's line_profiler的设计初衷。来自链接:

File: pystone.py
Function: Proc2 at line 149
Total time: 0.606656 s

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
   149                                           @profile
   150                                           def Proc2(IntParIO):
   151     50000        82003      1.6     13.5      IntLoc = IntParIO + 10
   152     50000        63162      1.3     10.4      while 1:
   153     50000        69065      1.4     11.4          if Char1Glob == 'A':
   154     50000        66354      1.3     10.9              IntLoc = IntLoc - 1
   155     50000        67263      1.3     11.1              IntParIO = IntLoc - IntGlob
   156     50000        65494      1.3     10.8              EnumLoc = Ident1
   157     50000        68001      1.4     11.2          if EnumLoc == Ident1:
   158     50000        63739      1.3     10.5              break
   159     50000        61575      1.2     10.1      return IntParIO

7
这是我写的装饰器:https://gist.github.com/kylegibson/6583590。如果你正在运行nosetests,请确保使用-s选项以便立即打印标准输出。 - Kyle Gibson
6
这个Python脚本是怎么样的才能产生这个输出?首先要导入line_profiler,然后呢? - Zhubarb
2
步骤1:安装line_profiler,命令为 pip install line_profiler 步骤2:在你想要进行性能分析的函数上方添加“@profile”修饰符 步骤3:运行以下命令生成.lprof文件:kernprof -l <YOUR SCRIPT> 步骤4:使用生成的.lprof文件运行以下命令以查看漂亮的结果:python -m line_profiler <YOUR LPROF FILE> - Mithun Kinarullathil
1
@MithunKinarullathil 这是唯一对我有效的方法。https://medium.com/uncountable-engineering/pythons-line-profiler-32df2b07b290 - zkilnbqi
为什么它不显示最后一列,即行内容,并说“您确定从与分析器相同的目录运行此程序吗?继续而不获取函数的内容。”可能的原因是什么? - knowledge_seeker
显示剩余4条评论

70
你也可以使用pprofile(pypi)。如果你想要对整个执行过程进行性能分析,它不需要修改源代码。你还可以通过两种方式对较大程序的子集进行性能分析:
  • 当到达代码中的特定点时切换性能分析,例如:

    import pprofile
    profiler = pprofile.Profile()
    with profiler:
        some_code
    # Process profile content: generate a cachegrind file and send it to user.
    
    # You can also write the result to the console:
    profiler.print_stats()
    
    # Or to a file:
    profiler.dump_stats("/tmp/profiler_stats.txt")
    
  • 使用统计分析来异步从调用栈中切换性能分析(需要一种触发此代码的方式在考虑的应用程序中,例如信号处理程序或可用的工作线程):

    import pprofile
    profiler = pprofile.StatisticalProfile()
    statistical_profiler_thread = pprofile.StatisticalThread(
        profiler=profiler,
    )
    with statistical_profiler_thread:
        sleep(n)
    # Likewise, process profile content
    

代码注释输出格式与行分析器类似:

$ pprofile --threads 0 demo/threads.py
Command line: ['demo/threads.py']
Total duration: 1.00573s
File: demo/threads.py
File duration: 1.00168s (99.60%)
Line #|      Hits|         Time| Time per hit|      %|Source code
------+----------+-------------+-------------+-------+-----------
     1|         2|  3.21865e-05|  1.60933e-05|  0.00%|import threading
     2|         1|  5.96046e-06|  5.96046e-06|  0.00%|import time
     3|         0|            0|            0|  0.00%|
     4|         2|   1.5974e-05|  7.98702e-06|  0.00%|def func():
     5|         1|      1.00111|      1.00111| 99.54%|  time.sleep(1)
     6|         0|            0|            0|  0.00%|
     7|         2|  2.00272e-05|  1.00136e-05|  0.00%|def func2():
     8|         1|  1.69277e-05|  1.69277e-05|  0.00%|  pass
     9|         0|            0|            0|  0.00%|
    10|         1|  1.81198e-05|  1.81198e-05|  0.00%|t1 = threading.Thread(target=func)
(call)|         1|  0.000610828|  0.000610828|  0.06%|# /usr/lib/python2.7/threading.py:436 __init__
    11|         1|  1.52588e-05|  1.52588e-05|  0.00%|t2 = threading.Thread(target=func)
(call)|         1|  0.000438929|  0.000438929|  0.04%|# /usr/lib/python2.7/threading.py:436 __init__
    12|         1|  4.79221e-05|  4.79221e-05|  0.00%|t1.start()
(call)|         1|  0.000843048|  0.000843048|  0.08%|# /usr/lib/python2.7/threading.py:485 start
    13|         1|  6.48499e-05|  6.48499e-05|  0.01%|t2.start()
(call)|         1|   0.00115609|   0.00115609|  0.11%|# /usr/lib/python2.7/threading.py:485 start
    14|         1|  0.000205994|  0.000205994|  0.02%|(func(), func2())
(call)|         1|      1.00112|      1.00112| 99.54%|# demo/threads.py:4 func
(call)|         1|  3.09944e-05|  3.09944e-05|  0.00%|# demo/threads.py:7 func2
    15|         1|  7.62939e-05|  7.62939e-05|  0.01%|t1.join()
(call)|         1|  0.000423908|  0.000423908|  0.04%|# /usr/lib/python2.7/threading.py:653 join
    16|         1|  5.26905e-05|  5.26905e-05|  0.01%|t2.join()
(call)|         1|  0.000320196|  0.000320196|  0.03%|# /usr/lib/python2.7/threading.py:653 join

请注意,pprofile 不依赖于代码修改,因此可以对顶级模块语句进行分析,从而可以分析程序的启动时间(导入模块、初始化全局变量等所需时间)。

它可以生成 cachegrind 格式的输出,因此您可以使用 kcachegrind 轻松浏览大型结果。

披露:我是 pprofile 的作者。


1
在确定性模式下,它确实具有显着的开销 - 这是可移植性的另一面。对于较慢的代码,我建议使用统计模式,它的开销非常小 - 但牺牲了跟踪精度和可读性。但这也可以是第一步:在统计模式下识别热点,生成触发已识别热点的较小案例,并使用确定性分析获取所有细节。 - vpelletier
至少对我来说,pprofile在Windows 10上不会生成时间信息。只有命中次数,这不是我想要的。 - zkilnbqi

21

仅是为了改进@Joe Kington的上述答案

对于 Python 3.x ,请使用 line_profiler


安装:

pip install line_profiler

使用方法:

假设你有一个程序 main.py,其中包含你想要针对时间进行性能分析的函数 fun_a()fun_b();你需要在函数定义之前加上装饰器 @profile。例如:

@profile
def fun_a():
    #do something

@profile
def fun_b():
    #do something more

if __name__ == '__main__':
    fun_a()
    fun_b()

可以通过执行 shell 命令对程序进行性能分析:

$ kernprof -l -v main.py

可以使用$ kernprof -h获取参数。

Usage: kernprof [-s setupfile] [-o output_file_path] scriptfile [arg] ...

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -l, --line-by-line    Use the line-by-line profiler from the line_profiler
                        module instead of Profile. Implies --builtin.
  -b, --builtin         Put 'profile' in the builtins. Use 'profile.enable()'
                        and 'profile.disable()' in your code to turn it on and
                        off, or '@profile' to decorate a single function, or
                        'with profile:' to profile a single section of code.
  -o OUTFILE, --outfile=OUTFILE
                        Save stats to <outfile>
  -s SETUP, --setup=SETUP
                        Code to execute before the code to profile
  -v, --view            View the results of the profile in addition to saving
                        it.

结果将被打印在控制台上,如下所示:

Total time: 17.6699 s
File: main.py
Function: fun_a at line 5

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    5                                           @profile
    6                                           def fun_a():
...


编辑:可以使用TAMPPA包解析分析器的结果。使用它,我们可以获得逐行所需的绘图: plot


这些指令是准确的,但图表是误导性的,因为 line_profiler 不会对内存使用进行分析(memory_profiler 会,但它经常失败)。如果您使用的是 Mac OS X 或 Linux,则建议使用(我的)Scalene分析器:pip install -U scalene,https://github.com/emeryberger/scalene-- 它同时对 CPU 时间和内存进行逐行分析(以及更多!)。 - EmeryBerger
你好 @emeryberger,所展示的图表是由一个新的包TAMPPA完成的,尽管它还存在一些问题。我知道有很多方法,感谢你分享其中之一。我建议在这里提交一个详细的答案 :) 你是否已经为'memory_profiler'提交了一个问题? - Pe Dro
我可以让Scalene分析器工作,你能提供一个例子吗? - MBV
line_profiler 对于异步函数无法正常工作。 - Abhishek
我正在运行这个程序,但它没有给出任何一个HitsTime per hit%Time的数值。 - Hercislife

13
你可以使用 line_profiler 包来帮助解决这个问题。

1. 首先安装该包:

    pip install line_profiler

2. 使用魔法命令将该包加载到您的Python/笔记本环境中

    %load_ext line_profiler

3. 如果你想为一个函数创建配置文件,则执行以下操作:

    %lprun -f demo_func demo_func(arg1, arg2)

如果您按照以下步骤操作,您将获得一个详细的格式化输出:)

Line #      Hits      Time    Per Hit   % Time  Line Contents
 1                                           def demo_func(a,b):
 2         1        248.0    248.0     64.8      print(a+b)
 3         1         40.0     40.0     10.4      print(a)
 4         1         94.0     94.0     24.5      print(a*b)
 5         1          1.0      1.0      0.3      return a/b

1
我按照完全相同的步骤进行了操作,但是它没有显示行内容,而是显示“你确定你从与分析器相同的目录运行此程序吗?没有该函数的内容。”可能的原因是什么? - knowledge_seeker
嗨@knowledge_seeker,你有答案了吗? - Mohammad Rijwan
@MohammadRijwan,最终我使用了Google Colab,在那里工作得很好。我想问题出在某些内部目录和文件之上。因此,我无法通过在笔记本电脑上安装所有Anaconda东西来解决它。但对我来说,Google Colab是一个临时的解决方法,因为我不需要担心目录路径,只需在其中安装我的行分析器即可。 - knowledge_seeker

1
PyVmMonitor有一个实时视图,可以帮助你(你可以连接到正在运行的程序并从中获取统计信息)。
请参见:http://www.pyvmmonitor.com/

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接