I often struggle to find bottlenecks in my cython code. How can I profile cython functions line-by-line?
 
    
    - 9,479
- 6
- 46
- 64
- 
                    Does the cython debugger allow you to pause it? Then you can do [*this*](http://stackoverflow.com/a/378024/23771). – Mike Dunlavey Nov 20 '15 at 17:07
3 Answers
Robert Bradshaw helped me to get Robert Kern's line_profiler tool working for cdef functions and I thought I'd share the results on stackoverflow.
In short, set up a regular .pyx file and build script and add the following before your call to 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
Furthermore, you need to define the C macro CYTHON_TRACE=1 by modifying your extensions setup such that
extensions = [
    Extension("test", ["test.pyx"], define_macros=[('CYTHON_TRACE', '1')])
]
A working example using the %%cython magic in the iPython notebook is here:
http://nbviewer.ipython.org/gist/tillahoffmann/296501acea231cbdf5e7
 
    
    - 9,479
- 6
- 46
- 64
- 
                    Very helpful, thanks. A detail: I've found that line_profiler states that the file profiled is the original .pyx file. I'm pretty sure I have everything pointing at the .pyd, so I suspect that the profiler just reads the content from the .pyd for display, while still getting the actual timings from the compiled version. – Giswok Dec 01 '15 at 17:18
- 
                    For reference, here are the [Cython docs on line tracing](http://docs.cython.org/src/tutorial/profiling_tutorial.html#enabling-line-tracing). – 0 _ Dec 06 '15 at 12:06
- 
                    There's also a more complete non-iPython example here: https://github.com/cython/cython/blob/master/tests/run/line_profile_test.srctree – deef Dec 23 '16 at 00:23
- 
                    5Has anyone tried this solution without using notebooks ? I tried bu it just ignores the cythonized code. Also if I try to decorate the function with ``@profile`` I'm not able do compile the file with disutils returning ``undeclared name not builtin: profile`` – Pedro Braz Aug 28 '17 at 19:11
- 
                    I've asked this question [here](https://stackoverflow.com/questions/45925921/cannot-use-line-profiler-with-cython) – Pedro Braz Aug 28 '17 at 19:15
- 
                    2Notice this changed in recent versions: https://github.com/cython/cython/issues/1497#issuecomment-256400972 – Pietro Battiston Jan 31 '18 at 15:55
- 
                    2@%%cython magic in iPython notebook use the following code since "from Cython.Compiler.Options import directive_defaults" is deprecated import Cython directive_defaults = Cython.Compiler.Options.get_directive_defaults() – tryptofame Nov 12 '18 at 15:24
- 
                    4directive_defaults = Cython.Compiler.Options.get_directive_defaults() # since "from Cython.Compiler.Options import directive_defaults" seems to be deprecated – tryptofame Jul 03 '19 at 12:59
- 
                    Since Cython 0.26 extensions contain relative paths to source files, so `line_profiler`/`kernprof` [can be unable to display them](https://github.com/cython/cython/issues/3929). You can workaround this by setting a drive root or a sibling folder as a working directory for cythonization. – EvgenKo423 Dec 03 '20 at 10:49
- 
                    Just a note that you MUST use `def`, I used `cpdef` and the timing results showed up empty. – TimD1 Oct 14 '21 at 04:10
- 
                    2As of 2023 this doesn't seem to be working at all. I made a new notebook which is basically the same as the old, though with the updated get_directive_defaults() thing. It all runs, but the actual output is simply `Timer unit: 1e-09 s`. Is there any way forward at all? https://nbviewer.org/gist/battaglia01/f138f6b85235a530f7f62f5af5a002f0?flush_cache=true – Mike Battaglia Feb 11 '23 at 13:33
- 
                    
- 
                    @G.Fougeron the problem is the latest version of line_profiler silently dropped support for Cython. The older version (3.5.1, I think?) still has it! Also you can try, e.g. py-spy or other profilers – Mike Battaglia May 11 '23 at 08:27
- 
                    
- 
                    Do you know whether there's an issue open for Cython support being dropped? – Till Hoffmann May 12 '23 at 18:17
While @Till's answer shows the way for profiling Cython-code using setup.py-approach, this answer is about ad-hoc profiling in IPython/Jupiter notebook and is more or less "translation" of Cython-documentation to IPython/Jupiter.
%prun-magic:
If %prun-magic should be used, then it is enough to set the Cython's compiler directive profile to True (here with example from Cython-documentation):
%%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 
Using global directive (i.e. # cython: profile=True) is a better way, than modifying the global Cython-state, because changing it will lead to extension being recompiled (which is not the case if the global Cython state is changed - the old cached version compiled with old global state will be reloaded/reused).
And now
%prun -s cumulative approx_pi(1000000)
yields:
        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
If the line profiler (i.e. %lprun-magic) should be used, then the Cython module should be compiled with different directives:
%%cython
# cython: linetrace=True
# cython: binding=True
# distutils: define_macros=CYTHON_TRACE_NOGIL=1
...
linetrace=True triggers creation of the tracing in the generated C-code and implies profile=True so it must not be set in addition. Without binding=True line_profiler doesn't have the necessary code-information and CYTHON_TRACE_NOGIL=1 is needed, so the line profiling is also activated when compiled with the C-compiler (and not thrown away by the C-preprocessor). It is also possible to use CYTHON_TRACE=1 if nogil-blocks should not be profiled on per-line basis.
Now it can be used for example as follows, passing the functions, which should be line-profiled via -f option (use %lprun? to get info about possible options):
%load_ext line_profiler
%lprun -f approx_pi -f recip_square approx_pi(1000000)
which yields:
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 the cpdef-function: it doesn't detect the function body correctly. In this SO-post, a possible workaround is shown.
One should be aware, that profiling (all above line profiling) changes the execution-time and its distribution compared with "normal" run. Here we see, that different times are needed depending on type of profiling, for the same function:
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
 
    
    - 32,758
- 6
- 90
- 153
Although I wouldn't really call it profiling, there is another option to analyze your Cython code by running cython with -a (annotate), this creates a webpage in which the main bottlenecks are highlighted. For example, when I forget to declare some variables:
After correctly declaring them (cdef double dudz, dvdz):
 
    
    - 9,825
- 5
- 47
- 73
- 
                    9True, not typing your variables will slow your code down. But `-a` will not give you any information about actual runtime but only whether you are making `python` calls. – Till Hoffmann Nov 20 '15 at 17:24
- 
                    But in my case, things like forgetting to declare a variable in porting Python to Cython code is what typically makes the code slow, and its a quick and simple way to test for those things. That's why I called it "_not_ _really_ _profiling_"; it's just a simple first code check/analysis. – Bart Nov 20 '15 at 18:49

