Cython
is essentially a Python to C translator. Cython allows you to use syntax similar to Python, while achieving speeds near that of C.
This post describes how to use Cython to speed up a single Python function involving ‘tight loops’. I’ll leave more complicated applications - with many functions and classes - for a later post.
Should I use Cython?
If you’re using Python and need performance there are a variety of options, see
quantecon
for a detailed comparison. And of course you could always choose a different language like Julia, or be brave and learn C itself.
While the static compilation approach of Cython may not be cutting edge, Cython is mature, well documented and capable of handling large complicated projects. Cython code lies behind many of the big Python scientific libraries including
scikit-learn
and
pandas
.
The example
Our example function evaluates a
Radial Basis Function
(RBF) approximation scheme. We assume each data point is a ‘center’ and use Gaussian type RBFs
so our function takes an input data array \( X\) of shape (N, D), a parameter array \( \beta\) of length N and a ‘bandwidth’ parameter \(\theta\) and return an array of fitted values \( \hat Y \) of length N.
So far all we’ve done is add some type declarations. For local variables we use the
cdef
keyword. For arrays we use
‘memoryviews’
which can accept numpy arrays as input.
Note that you don’t have to add type declarations in a
*.pyx
file. Any lines which use untyped variables will just remain in Python rather than being translated to C.
To compile we need a
setup.py
script, that looks something like this
OK, but we can do a better. With Cython there are a few ‘tricks’ involved in achieving good performance. Here’s the first one, if we type this in the terminal
cython fastloop.pyx -a
we generate a
fastloop.html
file which we can open in a browser
Lines highlighted yellow are still using Python and are slowing our code down. Our goal is get rid of yellow lines, especially any inside of loops.
Out first problem is that we’re still using the Python exponential function. We need to replace this with the C version. The main functions from
math.h
are included in the Cython
libc
library, so we just replace
from math import exp
with
fromlibc.mathcimportexp
Next we need to add some
compiler directives
, the easiest way is to add this line to the top of the file
Note that with these checks turned off you can get segmentation faults rather than nice error messages, so its best to debug your code before putting this line in.
Next we can consider playing with compiler flags (these are C tricks rather than Cython tricks as such). When using
gcc
the most important option seems to be
-ffast-math
. From my limited experience, this can improve speeds a lot, with no noticeable loss of reliability. To implement these changes we need to modify our
setup.py
file
#include <math.h>
staticuniondoubled;struct#ifdef LITTLE_ENDIAN
intj,i;#else
inti,j;#endif
}n;}_eco;#define EXP_A (1048576/M_LN2) /* use 1512775 for integer version */
#define EXP_C 60801 /* see text for choice of c values */
#define EXP(y) (_eco.n.i = EXP_A*(y) + (1072693248 - EXP_C), _eco.d)
From Cython its easy to call C code. Put the above code in
vfastexp.h
, then just add the following to our
fastloop.pyx
file