Rust vs C++
Many popular Python projects are using Rust, eg: Polars, Pydantic, uv, ruff, rye, orjson.
I couldn't find as many using C++, so I wanted to investigate using Rust and C++ from Python.
I'll use pybind11 for C++ and PyO3 for Rust. The associated source code is here.
Why Bother
Rust and C++ are generally faster than Python but are compiled and usually harder to write. Therefore being able to call Rust or C++ code allows for better performance without the associated downsides.
Other advantages of C++ and Rust include being strongly typed and parallelism (Python's GIL doesn't limit C++/Rust extensions).
Installation
Rust
Maturin makes PyO3 easy to use for new projects. In your new project directory:
$ pip install maturin
...
$ maturin init # follow the prompts
...
For existing projects you need to edit your pyproject.toml
and Cargo.toml
files. More details are here.
C++
The easiest way to install is via pip
:
$ pip install pybind11
Adding to your Python build system is slightly harder as you'll need a setup.py
like this (more details are here):
from glob import glob
from setuptools import setup
from pybind11.setup_helpers import Pybind11Extension, build_ext
ext_modules = [Pybind11Extension("python_example", sorted(glob("src/*.cpp")))]
...
There's also a one-liner to build the extension for simple use cases; I've used this approach in my example. There's a bug in the documentation for it, though I've opened a PR to fix it!
Writing the Extension
Rust
You need to 'decorate' the functions you want to expose, then register them:
use pyo3::prelude::*;
#[pyfunction]
fn rust_function(...) -> ...
#[pymodule]
fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(rust_function, m)?)?;
Ok(())
}
C++
Here you just need to register your functions and include the appropriate header file:
#include <pybind11/pybind11.h>
int cpp_function(...) { ... }
PYBIND11_MODULE(mandelbrot_cpp, m) {
m.doc() = "pybind11 example plugin"; // optional module docstring
m.def("cpp_function", &cpp_function, "A CPP function");
}
I came across a few downsides with this setup:
- It doesn't support kwargs as is
- My IDE complains about the
PYBIND11_MODULE
block
These can be solved with some extra work but the Rust setup didn't have similar issues.
Performance
On my (old) i5-2557M the sample script runs 40% faster using Rust than C++:
$ python main.py cpp
Language cpp took: 14.29s avg over 10 runs
$ python main.py rust
Language rust took: 8.72s avg over 10 runs
I'm sure I could make the C++ version just as fast (is it just some compiler flag I'm missing?), though I've made no efforts to optimise either script.
Takeaways
Overall the Rust setup is easier to use. Things just work out of the box and the associated tooling is simpler (just a few lines of config in Rust vs a large setup.py
for C++).
I also found the Rust version easier to write as the compiler gives clearer error messages, though the Rust learning curve is definitely steeper than for C++.
There are issues I haven't considered (eg scalability, maintainability, etc), especially for larger systems, however for relatively small projects Rust seems easier to use than C++ to create Python extensions, which may explain the recent popularity of such libraries written with Rust.
The Result
This post would be incomplete without plotting the results!
import numpy as np
import matplotlib.pyplot as plt
import mandelbrot_rs
result = np.array(mandelbrot_rs.generate_mandelbrot(-2, 2, -2, 2, 1000, 10)).reshape(1000, 1000)
plt.matshow(result)
plt.show()