Early Binding for Speed¶
Note
This page uses two different syntax variants:
Cython specific
cdef
syntax, which was designed to make type declarations concise and easily readable from a C/C++ perspective.Pure Python syntax which allows static Cython type declarations in pure Python code, following PEP-484 type hints and PEP 526 variable annotations.
To make use of C data types in Python syntax, you need to import the special
cython
module in the Python module that you want to compile, e.g.import cython
If you use the pure Python syntax we strongly recommend you use a recent Cython 3 release, since significant improvements have been made here compared to the 0.29.x releases.
As a dynamic language, Python encourages a programming style of considering classes and objects in terms of their methods and attributes, more than where they fit into the class hierarchy.
This can make Python a very relaxed and comfortable language for rapid development, but with a price - the âred tapeâ of managing data types is dumped onto the interpreter. At run time, the interpreter does a lot of work searching namespaces, fetching attributes and parsing argument and keyword tuples. This run-time âlate bindingâ is a major cause of Pythonâs relative slowness compared to âearly bindingâ languages such as C++.
However with Cython it is possible to gain significant speed-ups through the use of âearly bindingâ programming techniques.
For example, consider the following (silly) code example:
@cython.cclass
class Rectangle:
x0: cython.int
y0: cython.int
x1: cython.int
y1: cython.int
def __init__(self, x0: cython.int, y0: cython.int, x1: cython.int, y1: cython.int):
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
def area(self):
area = (self.x1 - self.x0) * (self.y1 - self.y0)
if area < 0:
area = -area
return area
def rectArea(x0, y0, x1, y1):
rect = Rectangle(x0, y0, x1, y1)
return rect.area()
cdef class Rectangle:
cdef int x0, y0
cdef int x1, y1
def __init__(self, int x0, int y0, int x1, int y1):
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
def area(self):
area = (self.x1 - self.x0) * (self.y1 - self.y0)
if area < 0:
area = -area
return area
def rectArea(x0, y0, x1, y1):
rect = Rectangle(x0, y0, x1, y1)
return rect.area()
In the rectArea()
method, the call to rect.area()
and the
area()
method contain a lot of Python overhead.
However, in Cython, it is possible to eliminate a lot of this overhead in cases where calls occur within Cython code. For example:
@cython.cclass
class Rectangle:
x0: cython.int
y0: cython.int
x1: cython.int
y1: cython.int
def __init__(self, x0: cython.int, y0: cython.int, x1: cython.int, y1: cython.int):
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
@cython.cfunc
def _area(self) -> cython.int:
area: cython.int = (self.x1 - self.x0) * (self.y1 - self.y0)
if area < 0:
area = -area
return area
def area(self):
return self._area()
def rectArea(x0, y0, x1, y1):
rect: Rectangle = Rectangle(x0, y0, x1, y1)
return rect._area()
cdef class Rectangle:
cdef int x0, y0
cdef int x1, y1
def __init__(self, int x0, int y0, int x1, int y1):
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
cdef int _area(self):
cdef int area = (self.x1 - self.x0) * (self.y1 - self.y0)
if area < 0:
area = -area
return area
def area(self):
return self._area()
def rectArea(x0, y0, x1, y1):
cdef Rectangle rect = Rectangle(x0, y0, x1, y1)
return rect._area()
Here, in the Rectangle extension class, we have defined two different area
calculation methods, the efficient _area()
C method, and the
Python-callable area()
method which serves as a thin wrapper around
_area()
. Note also in the function rectArea()
how we âearly bindâ
by declaring the local variable rect
which is explicitly given the type
Rectangle. By using this declaration, instead of just dynamically assigning to
rect
, we gain the ability to access the much more efficient C-callable
_area()
method.
But Cython offers us more simplicity again, by allowing us to declare dual-access methods - methods that can be efficiently called at C level, but can also be accessed from pure Python code at the cost of the Python access overheads. Consider this code:
@cython.cclass
class Rectangle:
x0: cython.int
y0: cython.int
x1: cython.int
y1: cython.int
def __init__(self, x0: cython.int, y0: cython.int, x1: cython.int, y1: cython.int):
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
@cython.ccall
def area(self)-> cython.int:
area: cython.int = (self.x1 - self.x0) * (self.y1 - self.y0)
if area < 0:
area = -area
return area
def rectArea(x0, y0, x1, y1):
rect: Rectangle = Rectangle(x0, y0, x1, y1)
return rect.area()
cdef class Rectangle:
cdef int x0, y0
cdef int x1, y1
def __init__(self, int x0, int y0, int x1, int y1):
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
cpdef int area(self):
cdef int area = (self.x1 - self.x0) * (self.y1 - self.y0)
if area < 0:
area = -area
return area
def rectArea(x0, y0, x1, y1):
cdef Rectangle rect = Rectangle(x0, y0, x1, y1)
return rect.area()
Here, we just have a single area method, declared as cpdef
or with @ccall
decorator
to make it efficiently callable as a C function, but still accessible from pure Python
(or late-binding Cython) code.
If within Cython code, we have a variable already âearly-boundâ (ie, declared explicitly as type Rectangle, (or cast to type Rectangle), then invoking its area method will use the efficient C code path and skip the Python overhead. But if in Cython or regular Python code we have a regular object variable storing a Rectangle object, then invoking the area method will require:
an attribute lookup for the area method
packing a tuple for arguments and a dict for keywords (both empty in this case)
using the Python API to call the method
and within the area method itself:
parsing the tuple and keywords
executing the calculation code
converting the result to a python object and returning it
So within Cython, it is possible to achieve massive optimisations by using strong typing in declaration and casting of variables. For tight loops which use method calls, and where these methods are pure C, the difference can be huge.