Cython Basics

Cython Basics

May 11, 2022·Jensen
Jensen

Getting Started


Concept of Cython


Cython is essentially Python with C data types.

With few exceptions, nearly all Python code is valid Cython code. The Cython compiler translates the code into equivalent C code that calls the Python/C API.

Since Cython’s parameters and variables can be declared with C data types, codes that operate on both Python values and C values can be freely mixed, with Cython automatically converting where necessary. Additionally, reference counting and error checking in Python are automatic, and Python’s exception handling mechanism, including try-except and try-finally, works just as well, even when operating on C data.


Your First Cython Program


Cython can accept almost all valid Python source files, so one of the biggest roadblocks on the journey to Cython is compiling the extension files.

Let’s start with the standard Python Hello, World:

print("Hello, World")

Save this code as helloworld.pyx. Now you need to create a setup.py, which is like a Makefile for Python, so setup.py should look like this:

from distutils.core import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("helloworld.pyx")
)

Then use the following command line to build the Cython file:

python setup.py build_ext --inplace

On Unix-like systems, this command will create a file called helloworld*.so in your local folder. On Windows, it is called helloworld*.pyd. Now run the Python interpreter and simply import this file as a regular Python module:

import helloworld
Hello, World

Congratulations! At this point, you already know how to create a Cython extension, but this example might not give you a clear sense of what advantages Cython has. So, let’s move on to more practical examples.

The Pyximport Module


If a Cython module does not need any external C libraries or special build setup, then it can directly be imported using the pyximport module. Developed by Paul Prescod, it allows for direct import of *.pyx files using import statements, without having to rerun setup.py every time the code is changed. The usage of pyximport module is as follows:

import pyximport; pyximport.install()
import helloworld
Hello, World

The pyximport module also supports experimental compilation of regular Python modules, automatically running Cython on every *.pyx and *.py module upon import, including the standard library and installed packages. Cython often fails to compile a large amount of Python modules, in which case the import mechanism will fall back and load the Python source module:

import pyximport; pyximport.install(pyimport=True)

(Note, this approach is now discouraged!)

Example: String to Integer


In a previous Python basics example, there was a simple string to integer example:

from functools import reduce

DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}

def str2int(s):
    def fn(x, y):
        return x * 10 + y
    def char2num(s):
        return DIGITS[s]
    return reduce(fn, map(char2num, s))

Now you can follow the Hello, World example above. First, change the file extension to str2int.pyx, then create a setup.py file:

from distutils.core import setup
from Cython.Build import cythonize

setup(
    ext_modules=cythonize("str2int.pyx"),
)

The command to create the extension is the same as for helloworld.pyx:

python setup.py build_ext --inplace

Using the extension is simple:

import str2int

str2int.str2int('2021111052')
2021111052

You can also use the pyximport module to import the extension:

import pyximport; pyximport.install()
import str2int

str2int.str2int('2021111052')
2021111052

Features of Cython


Here is a small example to introduce the features of Cython.

# Load Cython extension
%load_ext Cython
%%cython
def primes(int nb_primes):
    cdef int n, i, len_p
    cdef int p[1000]
    if nb_primes > 1000:
        nb_primes = 1000
        
    len_p = 0 # The current number of elements in p
    n = 2
    while len_p < nb_primes:
        # Is it a prime number?
        for i in p[:len_p]:
            if n % i == 0:  # If there's a factor, skip it, not a prime number
                break
        else:  # No factors, it's a prime number
            p[len_p] = n
            len_p += 1
        n += 1
    
    result_as_list = [prime for prime in p[:len_p]]
    return result_as_list

primes(10)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

As you can see, the beginning of the function looks like a normal Python function definition, except the parameter nb_primes is declared as an int type, meaning it will be converted to a C integer type when passed in (if the conversion fails, a TypeError is raised).

In the function body, the cdef statement is used to define some local C variables:

...
cdef int n, i, len_p  # Define some local C variables
cdef int p[1000]
...

During processing, the results are stored in a C array p, and finally copied into a Python list:

...
p[len_p] = n  # Results are stored in a C array
...
result_as_list = [prime for prime in p[:len_p]]  # Copied into a Python list
...
  • Note, in the example above, you can’t create too large an array because the array is allocated on the stack of the C function call, and these stack resources are limited. If you need larger arrays or the length of these arrays can only be determined at runtime, you can use Cython to allocate C memory or use Numpy arrays, etc., to improve efficiency.

In C, declaring a static array requires the size of the array to be known at compile time, so the program needs to ensure that the passed parameter does not exceed the size of the array, otherwise, it will throw a segmentation fault similar to C:

...
if nb_primes > 1000:
    nb_primes = 1000  # Prevent the passed parameter from exceeding the array size
...

It’s worth noting the following code:

...
for i in p[:len_p]:
   if n % i == 0:
       break
...

This code uses the candidate number divided by each found prime number to determine if the candidate number is not a prime number. Since no Python objects are referenced here, the loop is entirely translated into C code, thus greatly increasing the speed. Note the way the C array p is iterated:

...
for i in p[:len_p]:  # Although the loop is translated into C code, you can still use slicing like operating on a Python list, increasing efficiency
...

Before returning the results, they need to be copied into a Python list first, as Python cannot read C arrays. Cython can automatically convert many C types into Python types:

...
result_as_list = [prime for prime in p[:len_p]]  # Copied into a Python list
...

Note that, as with declaring a Python list, result_as_list is not explicitly declared, so it is treated as a Python object.

At this point, the basics of using Cython are clear, but it’s still worth exploring how much work Cython actually saves us. You can pass the parameter annotate=True to cythonize() to generate an HTML file:

from distutils.core import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("primes.pyx", annotate=True)
)

In the HTML file, you can see that the yellow lines represent interactions with Python, the more interactions, the deeper the color. White lines mean there’s no interaction with Python, and that part of the code was completely translated into C code.

These yellow lines will operate on Python objects, generate exceptions, or do some other more advanced operations, so they can’t be translated into simple and fast C code. Function declarations and returns used Python’s interpreter so these lines are also yellow.

001

Moreover, logically the statement if n % i == 0: could be directly implemented in C code, so why is it highlighted in yellow? It is found that Cython by default uses Python’s runtime division checks here. You can use the compiler directive cdivision=True to disable these checks.

Performance Comparison


For the above primes example, here is a Python version of the same program:

def primes_python(nb_primes):
    p = []
    n = 2
    while len(p) < nb_primes:
        # Is it a prime number?
        for i in p:
            if n % i == 0:
                break
                
        # If no break occurred in the loop
        else:
            p.append(n)
        n += 1
    
    return p
%%time
cython = primes(1000)
CPU times: user 1.62 ms, sys: 0 ns, total: 1.62 ms
Wall time: 1.62 ms
%%time
python = primes_python(1000)
CPU times: user 24.6 ms, sys: 0 ns, total: 24.6 ms
Wall time: 23.7 ms
cython == python
True

From the above comparison, it can be seen that the speed of Cython is almost tens of times superior to Python. Obviously, C has better support for CPU cache than Python, where everything is an object and exists in the form of dictionaries, which is not friendly to CPU cache. Generally, the speed of Cython will be between 2 and 1000 times that of Python, depending on the number of times the Python interpreter is called.

C++ Version of Primes

The above Cython calls are all to the C API, but Cython can also call C++ (some C++ standard libraries can be directly imported in Cython code). Below is the primes function using the vector from the C++ standard library:

# distutils: language=c++

from libcpp.vector cimport vector

def primes(unsigned int nb_primes):
    cdef int n, i
    cdef vector[int] p
    p.reserve(nb_primes)  # allocate memory for 'nb_primes' elements.
    
    n = 2
    while p.size() < nb_primes:  # vector's size() is similar to len()
        for i in p:
            if n % i == 0:
                break
        else:
            p.push_back(n)  # push_back is similar to append()
        n += 1
        
    return p  # When converting to Python objects, vector can be automatically converted to a Python list

The first line # distutils: language=c++ is a compiler directive, telling Cython to compile the code into C++, thus allowing the use of C++ features and C++ standard library (note, pyximport cannot compile Cython code into C++, setup.py is required).

It can be seen that the vector API of C++ is very similar to the list API of Python, often making them interchangeable in Cython.

For more details on using C++ in Cython, refer to Using C++ in Cython.

For more fundamentals of the Cython language, refer to Language Basics.

For the Chinese Cython documentation, refer to Cython Chinese Documentation.

Last updated on