Cython

Cython Book

Cython depends on CPython implementation (C/Python Interface)

[toc]

Chapter 1

EXAMPLE COMPARED (code)

WHY FASTER: function call overhead, looping, math ops, stack versus heap memory allocation.

Dynamic Lookup: all runtime definitions and object constructions -> Python interpreter checks types of object and find dunders -> unboxing objects to extract the underlying C types, and only THEN can the ops occur! C and Cython already compiled types, the addition compiles to one machine code instruction.

Cython Wrapping C

// cfib.h

double cfib(int n);
# Cython wrapper

cdef extern from "cfib.h":
    double cfib(int n)
    
def fib(n):
    """Returns the nth Fib"""
    return cfib(n)
from wrap_fib import fib
help(fib)
fib(90)

Chapter 2: Compiling and Running Cython Code

Ways of Compilation

The Pipeline

First - cython compiler transforms Cython source into optimised and platform-independent C or C++ !!

Second - compile the C/C++ source into SHARED LIB with a standard C/C++ compiler (platform dependent!!) .so file on Linux or MacOS and dynamic lib .pyd on Windows !!

Flags passed to C/C++ compiler ensure this shared lib a full-fledged Python module - EXTENSION MODULE importable as if pure Python

Cython compiler a source-to-source compiler with highly optimised code

Once having C compiler and cython compiler (pip install cython) in place, ready to follow along with distutils and pyximport methods

STANDARD - DISTUTILS + CYTHONIZE

One of many features of distutils is its ability to compile C source into an extension module, the SECOND stage in the pipeline. It manages ALL platform, architecture, and Python-version details !!

FIRST stage is handled by cythonize cmd, compiling Cython source (options) into C/C++ source file !!

EXPLICIT control the pipeline requiring writing small Python script and run it.

distutils script

Using Ext-Mod

Interactive IPython %%cython magic

%%load_ext cythonmagic

%%cython
def...

Compiling On-the-Fly with pyximport

import pyximport
pyximport.install()

import fib
fib.__file__ # .../.pyxbld/lib.macos-...

Controlling and Managing Depens

Example with External Depens

Rolling Own and Compiling by Hand

For the sake of completeness, from pyx source to end

CFLAGS=$(python-config --cflags)
LDFLAGS=#(python-config --ldflags)
cython fib.pyx # --> fib.c
gcc -c fib.c ${CFLAGS} # --> fib.o
gcc fib.o -o fib.so -shared ${LDFLAGS} # --> fib.so

Other Build Systems

Many build tools better than distutils for depens management.

CMake

folding Cython to standard CMake-compiled project.

Make

Recomm to query Python interpreter to find right config/flags per above. (same as distutils.sysconfig)

Other config available via get_config_var

Standalone Executables

# irrationals.py

from math import pi, e

print...

Compiler Directives

Comments inside code to control compilation configs. Four scopes in total.

Chapter 3: Cython in Depth

WHY: RUNTIME INTERPRETATION VS. PRE-COMPILATION, & DYNAMIC VS. STATIC TYPING

The WHY

Interpreted vs. Compiled Execution

Dynamic vs. Static Typing

ST Declaration

C Pointers

cdef int *p_int
cdef float** pp_float = NULL

cdef int *a, *b

Mixing ST and DT (Power of Cython)

ST with Python Type!

ST for Speed

Reference Counting and Static String Types

Cython’s 3 kinds of FUNC

Python func with def

C Funcs with cdef

Combining as cpdef

Exception Handling

Embedsignature Compiler Directive

Generated C Code

def mult(a, b):
    return a * b

# compare with ST 
...(int a, int b):
    ...

cython mult.pyx => mult.c

Several thousand lines long!

/* "mult.pyx":3
*
* def mult(a, b):
*	return a * b 		# <<<<<<<<<
*/
__pyx_t_1 = PyNumer_Multiply(__pyx_v_a, __pyx_v_b);
if (unlikely(!__pyx_t_1)) {
    __pyx_filename = __pyx_f[0];
    __pyx_lineno = 3;
    __pyx_clineno = __LINE__;
    goto __pyx_l1_error;
}

# compare with ST
...
    __pyx_t_1 = __Pyx_PyInt_From_int((__pyx_v_a * __pyx_v_b));

Type Coercion and Casting

Structs, Unions, Enums

Type Aliasing with ctypedef

FUSED TYPES AND GENERIC CODING

  • Cython has novel typing fused types allowing reference to several reltaed types with a single type definition

    # max for integral values
      
    from cython cimport integral # fused type for C short, int, long scalar types
      
    cpdef integral integral_max(integral a, integral b):
        return a if a >= b else b
      
    # Cython creates 3 integral_max: one for both as shorts, as ints, as longs
    # using long if called from Python
    # when called from other Cython ode, check arg type at compile time to set which to use
      
    # allowed uses
    cdef allowed():
        print(integral_max(<short>1, <short>2))
        print(integral_max(<int>1, <int>2))
        ...
      
    # CANNOT mix for the same fused type from other Cython code
      
    # to generalise support floats and doubles, cannot use cython.numeric fused type since complex are not comparable
    # Can create own fused type to group integral and floating C types
      
    cimport cyhton
      
    ctypedef fused integral_or_floating:
        cython.short
        cython.int
        cython.long
        cython.float
        cython.double
          
    cpdef integral_or_floating generic_max(integral_or_floating a,
                                          integral_or_floating b):
        return a if a >= b else b
      
    # five spec, one each C type in ctypedef fused block
    

Loop and While

Guidelines for Efficient Loops

Cython Preprocessor (C macro)

DEF E = 2.718281828459045
DEF PI = 3.14159263589

def feymans_jewel():
    """Returns e**(i*pi) + 1. Should be ~0.0"""
    return E ** (1j * PI) + 1.0

Chapter 4 Practice: N-Body Simulation

# Python

def main(n, bodies=BODIES, ref='sun'):
    # takes number of steps n to integrate initial conditions 
    # of bodies with reference body i.e. the Sun
    
    # gets list of all bodies and makes pairs of all for ease of iteration
    system = list(bodies.values()) 
    pairs = combinations(system)

    # correct Sun's momentum to stay at the Cen of Mass
    offset_momentum(bodies[ref], system)
    
    # compute and print system's total energy
    report_energy(system, pairs)
    
    # Symplectic integrators good at conserving energy, used to test accuracy
    # after getting init energy, core compute in time step
    # unit of time is mean solar day, distance is one astro-unit, solar mass 
    advance(0.01, n, system, pairs)
    report_energy(system, pairs) # should be close to pre-advance

Python Data Structure and Organisation

Running Cythonized version

Conversion Summary

  1. Profile using cProfile or %run to see most runtime
  2. Inspect hotspots for nested loops, numeric-heavy ops, nested Python containers, all can be easily converted with Cython to use more efficient C constructs (above case have all)
  3. Use Cython to declare C data structures equivalent to Python’s. Create converters if needed to transform Python data to C data. In the N-body simu, body_t struct to represent nested that-long-name, getting better data locality and fast access! Two converters, make_cbodies, make_pybodies to convert to and fro both
  4. Convert hotspots to use C data, removing Python data from nested loops to the extent possible - ensuring all variables used in nested loop (including loop variables) are ST!
  5. Test to check semantics intact

Detail Code Study

# cython setup.py
from distutils.core import setup
from Cython.Build import cythonize

setup(name="nbody",
      ext_modules=cythonize("nbody.pyx"))

# global definition the same; maybe Cython could do typed

# Python def BODIES
BODIES = {
    'sun': ([....]),
    'jupiter': ...
}
# Cython need struct
cdef struct body_t:
    double x[3]
    double v[3]
    double m

DEF NBODIES = 5

BODIES = {
    ... # same as python
}

# Python
def advance... # see above

# Python
def report_energy(bodies, pairs):

    e = 0.0

    for (((x1, y1, z1), v1, m1),
            ((x2, y2, z2), v2, m2)) in pairs:
        dx = x1 - x2
        dy = y1 - y2
        dz = z1 - z2
        e -= (m1 * m2) / ((dx * dx + dy * dy + dz * dz) ** 0.5)
    for (r, [vx, vy, vz], m) in bodies:
        e += m * (vx * vx + vy * vy + vz * vz) / 2.
    print("%.9f" % e)
    
# Cython
def report_energy(bodies):
    # no `pairs` arg but compute internally
    e = 0.0
    paris = combinations(bodies)
    for ...
    # the same
    
# Cython auxiliaries
cdef void make_cbodies(list bodies, body_t *cbodies, int num_cbodies):
    cdef body_t *cbody
    for i, body in enumerate(bodies):
        if i >= num_cbodies:
            break
        (x, v, m) = body
        cbody = &codies[i]
        cbody.x[0], cbody.x[1], cbody.x[2] = x
        cbody.v[0], cbody.v[1], cbody.v[2] = v
        cbodies[i].m = m
        
cdef list make_pybodies(body_t *cbodies, int num_cbodies):
    pybodies = []
    for i in range(num_cbodies):
        x = [cbodies[i].x[0], cbodies[i].x[1], cbodies[i].x[2]]
        v = [cbodies[i].v[0], cbodies[i].v[1], cbodies[i].v[2]]
        pybodies.append((x, v, cbodies[i].m))
    return pybodies

# the same 
def offset_momentum(ref, bodies):

    px = py = pz = 0.0

    for (r, [vx, vy, vz], m) in bodies:
        px -= vx * m
        py -= vy * m
        pz -= vz * m
    (r, v, m) = ref
    v[0] = px / m
    v[1] = py / m
    v[2] = pz / m

    
# python
def main(n, bodies=BODIES, ref='sun'):
    system = list(bodies.values())
    pairs = combinations(system)
    offset_momentum(bodies[ref], system)
    report_energy(system, pairs)
    advance(0.01, n, system, pairs)
    report_energy(system, pairs)

if __name__ == '__main__':
    main(int(sys.argv[1]), bodies=BODIES, ref='sun')

# cyhton
def main(n, bodies=BODIES, ref='sun'):
    system = list(bodies.values())
    offset_momentum(bodies[ref], system)
    report_energy(system)
    system = advance(0.01, n, system)
    report_energy(system)

# DIFF: separete run_nbody.py using pyx (after build)
import sys
from nbody import main
main(int(sys.argv[1]))
# Makefile
all: nbody.so
nbody.so: nbody.pyx
    cython -a nbody.pyx
    python setup.py build_ext -fi
clean:
    -rm -r build nbody.so nbody.c nbody.html

Chapter 5 Extension Type

Comparing Py-Class and Ext Type

# Example
# when compiled to C, resulting cls just regular Python, NOT ext_type!
# small boost due to pre-compilation skipping interpreter overhead
# real boost comes with ST
cdef class Particle:
    cdef double mass, position, velocity
	# the rest of __init__ and get_momentum(self) INTACT

Type Attributes and Access Control

C-Level INIT and FINAL

cdef and cpdef Methods

Inheritance and Subclassing

Casting and Subclass

Extension Type Objects and None

Extension Type Properties

Special Methods Are Even More SPECIAL

Arithmetic

SUMMARY

CYTHON MELDS EXT_TYPE DEFINITION

  1. allows easy and fast access to an instance’s C data and methods
  2. memory efficient
  3. allows control over attribute visibility
  4. can be subclassed from Python
  5. workds with existing built-in types and other extension types

USE: TO WRAP C STRUCTS, FUNCTIONS, AND C++ CLASSES TO PROVIDE NICE OBJECT-ORIENTED INTERFACES TO EXTERNAL LIBRARIES.

Chapter 6: Organising Cython Code

Declaration File

Declarations and Definitions

# simulator.pyx

cdef class State:
    def __cinit__(...):
        # ...
	def __dealloc__(...):
        # ...
	cpdef real_t momentum(self):
        # ...

def setup(input_fname):
    #...
cpdef run(State st):
	# ...calls step function repeatedly...
cpdef int step(State st, real_t timestep):
	# ...advance st one time step...
def output(State st):
    #...

cimport Statement

Predefined Definition Files

Using cimport with a module in a package

from libc cimport math
math.sin(3.14)

# cimport imports module-like math namespace from libc packages allowing dotted access
# C funcs declared in math.h C standard library
# multiple named cimports

from libc.stdlib cimport rand, srand, qsort, malloc, free
cdef int *a = <int*>malloc(10 * sizeof(int))

from libc.string cimport memcpy as c_memcpy

from libcpp.vector cimport vector
cdef vector[int] *vi = new vector[int](10)

# invalid same namespace

Include Files and Include Statement

Organising and Compiling Cython Modules Inside Python Packages

Chapter 7: Wrapping C

Declaring External C code in Cython

Bare extern Declarations - Cython supports extern cdef extern external_declaration, used in this manner, Cython will place it (func signature, variable, struct, union or other such C declaration) in the generated source code with extern modifier - must match C declaration

This style of external declaration is not recommended, as it has the same drawbacks as using extern in C directly, extern block is preferred !!

Cython Does NOT Automate Wrapping

Declaring External C Func and typedefs

Declaring and Wrapping C structs, unions, enums

Wrapping C Functions

Wrapping C structs with Extension Types

More Control - constant, modifier, output

Error Exception - Callback

Callbacks and Exception Propagation

Wrapping C++

Chapter 9: Profiling Tools


code · notebook · prose · gallery · qui et quoi? · main