Подтвердить что ты не робот

Как профилировать функции cython по очереди

Я часто пытаюсь найти узкие места в моем коде cython. Как я могу профилировать функции cython по очереди?

4b9b3361

Ответ 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