Chapter 13. Cython in Context
The most important thing in the programming language is the name. A language will not succeed without a good name. I have recently invented a very good name and now I am looking for a suitable language.
— D. Knuth
In this last chapter, now that we have invested blood, sweat, and carpal tunnel syndrome in learning the depth and breadth of the Cython language, it is worthwhile to consider Cython in relation to other projects. As we have seen, Cython does many things well: it brings optional static typing to the Python language, it compiles Python to C, and it enables easy interoperability between Python, C, and C++. The greater Python world is diverse, and it is no surprise that many projects—new and old— overlap with Cython in some way. How does Cython match up, and what makes it relevant in the midst of these other options? When embarking on a new Python project, why should we use Cython?
Cython Versus Project X
Cython’s predecessor, Pyrex, is firmly in the traditional ahead-of-time compiler camp, and Cython inherits much of its design. But Cython has extended to acquire just-in-time compilation features, as we saw in Chapter 2.
The Cython core developers have discussed generalizing Cython to target other backend languages, but C and C++ are and will be Cython’s primary targets.
Cython’s close ties to C and C++ come with many advantages:
§ C and C++ are extremely well established languages with many high-quality free and commercial compilers, and these compilers have benefited from several decades of optimization effort to generate very efficient binaries.
§ C and C++ (and, in HPC contexts, Fortran) are the go-to languages as soon as performance is an important consideration, meaning many existing high-performance libraries are written in these languages.
§ C and C++ are still actively growing and evolving; both languages have had recently updated standards to incorporate new features and expanded standard libraries.
C and C++, as a rule, choose control and performance over safety and ease of use. For instance, neither language provides automatic garbage collection (although there are ways to approach that via C++ smart pointers). By providing a Python-like language that is compiled to C and C++, Cython simplifies the task of programming in and with these languages.
Other Ahead-of-Time Compilers for Python
Three other ahead-of-time Python compiler projects are worth noting:
Nuitka is a more recent Python-to-C++ compiler that supports all Python constructs from 2.7 through 3.3. One of its main focuses is on automatically compiling an entire Python application into a binary executable or extension module. It has future plans for automatic type inference,ctypes integration, and some way to inform Nuitka of type information it cannot determine unaided.
Shedskin is an experimental Python-to-C++ compiler that compiles to a standalone binary without any CPython dependencies. It places some restrictions on the Python it can compile—reassigning a variable to an object with a different type is not allowed. This restriction allows Shedskin to use sophisticated type inference to determine the C++ type of a variable. Like Nuitka, it works with pure Python and therefore does not support static typing, but its type inference mitigates this to some extent. It does not support calling out to external C++ code, however.
Like Shedskin, Pythran is a Python-to-C++ compiler for a subset of the Python language. Like Cython, Pythran provides a way to add type information to Python code via inline comments to help it generate more efficient C++. It also has features to make use of SIMD instructions and automatic parallelization over multiple cores. It is firmly in the scientific computing camp, and focuses its optimizations there. It has some support for NumPy arrays, but not quite as much as Cython. It does not support interfacing with external libraries.
Each of these projects provides a way to automatically generate C or C++ source from Python code and automatically compile an extension module. None goes as far as Cython does by extending the Python language, preferring instead to maintain pure-Python compatibility. None has the longevity, widespread user base, or breadth of Cython. In particular, all ignore interfacing existing C and C++ code with Python, which is one of Cython’s major strengths.
Python Wrapper Projects
As discussed in Chapters 7 and 8, Cython has first-class support for interfacing Python with external C and C++ libraries. When combined with the rest of the Cython language—particularly its static typing features—this support makes Cython a powerful tool to provide highly optimized wrappers.
Several standalone projects automate the process of generating Python bindings for C and C++. The best-known projects in this space are SWIG and Boost.Python:
SWIG is the king of the hill with regard to automatically wrapping C and C++. It has been around since the 1990s, can generate wrapper code for 20 different target languages—both mainstream and obscure—and offers sophisticated customization features to the end user via typemaps. For all its strengths, it can be difficult to use for advanced needs. Cython cannot hold a candle to SWIG’s full breadth of wrapping prowess. But Cython does have strengths when compared to SWIG: because Cython focuses specifically on Python, its wrappers are better optimized and have less runtime overhead when compared to SWIG’s. Also, because Cython provides a full Python-like language to help wrap external code, it can be easier to use in advanced cases. SWIG automates the wrapping process almost entirely, and is therefore easier to use than Cython to wrap large libraries. If users require extensive customization when wrapping a library, however, the advantage of SWIG over Cython is less clear.
The Boost project has the Boost.Python library to enable easy interoperability between C++ and Python. It uses sophisticated C++ template metaprogramming to generate Python wrappers for C++ libraries. As with Cython (and unlike with SWIG), each C++ class, function, and method must be specified separately to be wrapped. To help with this, it provides high-level C++ constructs to help in the wrapping effort. Boost.Python also provides features to allow the direct manipulation of Python objects in C++ in a high-level way.
There are several other wrapping tools for Python, but SWIG and Boost.Python are the main contenders. Neither can compile existing Python code to C or C++, so they lack what Cython provides there. Each can be thought of as providing an interfacing domain-specific language to control how the wrappers are generated. Cython, in comparison, has features to describe the external interface, but uses the full Python and Cython languages to accomplish the interfacing.
To further narrow the gap, an up-and-coming project named XDress automatically generates Cython wrappers for C and C++ libraries, making Cython easier to use for large C- and C++- wrapping projects.
Just-in-Time Compilers for Python
At the other end of the spectrum are the just-in-time (JIT) Python compilers. These stress ease of use and automatic compilation at runtime, with very little user input required. Python JITs are a very active area of development, especially since the advent of the LLVM project.
Some of the more widely known Python JITs are:
PyPy is the oldest Python JIT compilation project in widespread use. It offers ease of use and improved performance for unmodified Python code. It can yield nice speedups for certain classes of operations, particularly operations on built-in Python containers. PyPy does not offer the same level of control that Cython provides—PyPy does what it does, and if the speedup is not satisfactory, there is little that can be done. Cython, in contrast, often requires more effort to provide static type information, but it also allows the end user to try many different approaches, moving more code into C or C++ to improve performance. PyPy’s extension module support—including NumPy, SciPy, and the like—has traditionally been its greatest weakness, although efforts are under way to address this. Because Cython merges Python and C, and because generating extensions is its modus operandi, it is in a much better position when it comes to interfacing.
Numba is an LLVM-based JIT compiler that is focused on speeding up array-oriented and math-heavy Python code. Like all JIT compilers, it provides this speedup automatically from within a single code base, so it is easier to use than Cython in this respect. Cython can achieve nice speedups for this same subset of operations, but it requires some static type information to help the cython compiler generate efficient code. On the other hand, Cython’s ability to speed up non-numeric Python code (using the built-in containers and non-numeric data types) allows Cython to speed up general Python code that is not Numba’s primary focus.
Pyston is another Python JIT compiler project, currently in its infancy, that aims to speed up general Python code, like PyPy. It takes a different approach than PyPy, however, and like Numba, it is based on the LLVM project. From the outset it aims to support interoperability with CPython extension modules.
In general, Cython is not as easy to use as JIT compilers, given that it typically relies on inline static type declarations to generate efficient code. (The pyximport package and the %%cython magic support in IPython do provide some degree of automatic compilation for Cython code, making Cython easier to explore.)
On the other hand, because JIT compilers stress ease of use and work with pure-Python code, they do not provide the same level of control that a hybrid language like Cython does. Cython allows the user to determine where on the Python-to-C spectrum to implement an algorithm; because of this, it is often possible to achieve better performance by pushing more code into C or C++. Cython also provides code annotations to help indicate where code is likely to be inefficient. When we are using a JIT compiler, it is up to the compiler implementation to provide all optimizations. If the performance is not satisfactory, then end users have little at their disposal to remedy the situation.
Cython also does not place any runtime dependencies on end users (other than the Python runtime itself). This is in contrast to JIT compilers, which require the JIT compiler infrastructure at runtime. Because Cython generates a standalone C or C++ source file, a package developer can distribute just these generated files (or precompiled binaries) to end users. The extension module requires only the Python runtime and any wrapped library components; Cython itself is not required when we are running a Cython-generated extension module.
Cython is difficult to categorize succinctly: it is an ahead-of-time compiler, but the pyximport package and %%cython IPython magic (Chapter 2) introduce aspects of just-in-time compilers. Cython has powerful features to call into external C and C++ libraries, making it competitive with specialized binding generator projects like SWIG and Boost.Python. Perhaps the best way to think of Cython is in the name itself: it fluidly blends C and C++ with Python. It combines capabilities from all the major topics covered in this chapter, and it does so in such a way that all components work well with one another.
The open source Python world has widely adopted Cython, for good reason: it has demonstrated its breadth and depth of features time and again in this competitive environment, where life and death are based on technical merit and overall value. Large and widely used projects such as Sage, Pandas, scikits-learn, scikits-image, and lxml use Cython to provide highly optimized algorithms for all of their performance-critical components. Projects such as MPI4Py, PETSc4Py, and (again) Sage use Cython for its powerful wrapping features. Cython is also used pervasively in research and closed source projects where performance improvements and interfacing Python with C or C++ are necessary.
With this one multifaceted tool in hand, we can confidently bring Python’s dynamism to C and C++, and bring the performance of C and C++ to Python.