Skip to main content

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:

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()