我经常很难找到cython
代码中的瓶颈。如何逐行对cython
函数进行性能分析?
Robert Bradshaw 帮我使 Robert Kern 的 line_profiler
工具可以用于 cdef
函数,我想在 stackoverflow
上分享结果。
简而言之,设置一个常规的 .pyx
文件和构建脚本,在调用 cythonize
之前添加以下内容。
# Thanks to @tryptofame for proposing an updated snippet
from Cython.Compiler.Options import get_directive_defaults
directive_defaults = get_directive_defaults()
directive_defaults['linetrace'] = True
directive_defaults['binding'] = True
extensions
设置来定义C宏CYTHON_TRACE=1
。extensions = [
Extension("test", ["test.pyx"], define_macros=[('CYTHON_TRACE', '1')])
]
import Cython
directive_defaults = Cython.Compiler.Options.get_directive_defaults()
- tryptofameTimer unit: 1e-09 s
。有任何前进的方式吗?https://nbviewer.org/gist/battaglia01/f138f6b85235a530f7f62f5af5a002f0?flush_cache=true - Mike Battaglia虽然 @Till's answer 展示了使用 setup.py
方法对 Cython 代码进行性能分析的方法,但本答案则介绍如何在 IPython/Jupiter notebook 中进行临时性能分析,更或者说是将 Cython 文档 翻译到 IPython/Jupiter 上。
%prun
魔法命令:
如果想要使用 %prun
魔法命令,则只需将 Cython 的编译指令 profile
设置为 True
即可(这里使用的是来自 Cython 文档的例子):
%%cython
# cython: profile=True
def recip_square(i):
return 1. / i ** 3
def approx_pi(n=10000000):
val = 0.
for k in range(1, n + 1):
val += recip_square(k)
return (6 * val) ** .5
# cython: profile=True
)是比修改全局Cython状态更好的方式,因为修改会导致扩展重新编译(如果修改全局Cython状态,则不会发生这种情况 - 旧的缓存版本编译时使用旧的全局状态将被重新加载/重用)。现在。%prun -s cumulative approx_pi(1000000)
1000005 function calls in 1.860 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 1.860 1.860 {built-in method builtins.exec}
1 0.000 0.000 1.860 1.860 <string>:1(<module>)
1 0.000 0.000 1.860 1.860 {_cython_magic_404d18ea6452e5ffa4c993f6a6e15b22.approx_pi}
1 0.612 0.612 1.860 1.860 _cython_magic_404d18ea6452e5ffa4c993f6a6e15b22.pyx:7(approx_pi)
1000000 1.248 0.000 1.248 0.000 _cython_magic_404d18ea6452e5ffa4c993f6a6e15b22.pyx:4(recip_square)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
%lprun
-magic
如果要使用行级分析器(即%lprun
-magic),则需要使用不同的编译指令来编译Cython模块:
%%cython
# cython: linetrace=True
# cython: binding=True
# distutils: define_macros=CYTHON_TRACE_NOGIL=1
...
linetrace=True
触发在生成的C代码中创建跟踪,并暗示profile=True
,因此不必再额外设置。如果没有binding=True
,则line_profiler没有必要的代码信息,需要CYTHON_TRACE_NOGIL=1
,因此当使用C编译器(而不是C预处理器)编译时,也会激活行分析。还可以使用CYTHON_TRACE=1
,如果不希望以每行为基础对nogil块进行分析。
现在可以按以下方式使用它,通过-f
选项传递应该进行行分析的函数(使用%lprun?
获取有关可能选项的信息):
%load_ext line_profiler
%lprun -f approx_pi -f recip_square approx_pi(1000000)
得出的结果为:
Timer unit: 1e-06 s
Total time: 1.9098 s
File: /XXXX.pyx
Function: recip_square at line 5
Line # Hits Time Per Hit % Time Line Contents
==============================================================
5 def recip_square(i):
6 1000000 1909802.0 1.9 100.0 return 1. / i ** 2
Total time: 6.54676 s
File: /XXXX.pyx
Function: approx_pi at line 8
Line # Hits Time Per Hit % Time Line Contents
==============================================================
8 def approx_pi(n=10000000):
9 1 3.0 3.0 0.0 val = 0.
10 1000001 1155778.0 1.2 17.7 for k in range(1, n + 1):
11 1000000 5390972.0 5.4 82.3 val += recip_square(k)
12 1 9.0 9.0 0.0 return (6 * val) ** .5
line_profiler
有一个小缺陷,对于cpdef
函数无法正确检测函数主体。在这篇SO帖子中,展示了一种可能的解决方法。Method (N=10^6): Running Time: Build with:
%timeit 1 second
%prun 2 seconds profile=True
%lprun 6.5 seconds linetrace=True,binding=True,CYTHON_TRACE_NOGIL=1
-a
不会提供有关实际运行时间的信息,只会告诉你是否正在进行python
调用。 - Till Hoffmann