Я часто пытаюсь найти узкие места в моем коде cython
. Как я могу профилировать функции cython
по очереди?
Как профилировать функции cython по очереди
Ответ 1
Роберт Брэдшоу помог мне получить инструмент Роберта Керна line_profiler
, работающий для функций cdef
, и я решил поделиться результатами на stackoverflow
.
Короче, настройте обычный файл .pyx
и создайте script и добавьте следующее до вашего вызова cythonize
.
from Cython.Compiler.Options import directive_defaults
directive_defaults['linetrace'] = True
directive_defaults['binding'] = True
Кроме того, вам необходимо определить макрос C CYTHON_TRACE=1
, изменив настройку extensions
таким образом, чтобы
extensions = [
Extension("test", ["test.pyx"], define_macros=[('CYTHON_TRACE', '1')])
]
Рабочий пример с использованием магии %%cython
в записной книжке iPython
находится здесь:
http://nbviewer.ipython.org/gist/tillahoffmann/296501acea231cbdf5e7
Ответ 2
Хотя я бы не назвал это профилированием, есть еще один способ проанализировать ваш Cython-код, запустив cython
с помощью -a
(аннотировать), это создаст веб-страницу, на которой выделены основные узкие места. Например, когда я забываю объявить некоторые переменные:
После правильного объявления их (cdef double dudz, dvdz
):
Ответ 3
Хотя @Till answer показывает способ профилирования Cython-кода с использованием setup.py
-approach, этот ответ о специальном профилировании в записной книжке IPython/Jupiter и является более или менее "переводом" Cython-документация для IPython/Jupiter.
%prun
-magic:
Если нужно использовать %prun
-magic, то достаточно установить директиву компилятора 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´ has however a minor hiccup with
cpdef '-function: неправильно определяет тело функции. В этом 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