Performance
To achieve the best possible performance, it is useful to be aware of several tricks and sharp edges concerning PyO3's API.
extract
versus downcast
Pythonic API implemented using PyO3 are often polymorphic, i.e. they will accept &Bound<'_, PyAny>
and try to turn this into multiple more concrete types to which the requested operation is applied. This often leads to chains of calls to extract
, e.g.
#![allow(dead_code)]
use pyo3::prelude::*;
use pyo3::{exceptions::PyTypeError, types::PyList};
fn frobnicate_list<'py>(list: &Bound<'_, PyList>) -> PyResult<Bound<'py, PyAny>> {
todo!()
}
fn frobnicate_vec<'py>(vec: Vec<Bound<'py, PyAny>>) -> PyResult<Bound<'py, PyAny>> {
todo!()
}
#[pyfunction]
fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
if let Ok(list) = value.extract::<Bound<'_, PyList>>() {
frobnicate_list(&list)
} else if let Ok(vec) = value.extract::<Vec<Bound<'_, PyAny>>>() {
frobnicate_vec(vec)
} else {
Err(PyTypeError::new_err("Cannot frobnicate that type."))
}
}
This suboptimal as the FromPyObject<T>
trait requires extract
to have a Result<T, PyErr>
return type. For native types like PyList
, it faster to use downcast
(which extract
calls internally) when the error value is ignored. This avoids the costly conversion of a PyDowncastError
to a PyErr
required to fulfil the FromPyObject
contract, i.e.
#![allow(dead_code)]
use pyo3::prelude::*;
use pyo3::{exceptions::PyTypeError, types::PyList};
fn frobnicate_list<'py>(list: &Bound<'_, PyList>) -> PyResult<Bound<'py, PyAny>> { todo!() }
fn frobnicate_vec<'py>(vec: Vec<Bound<'py, PyAny>>) -> PyResult<Bound<'py, PyAny>> { todo!() }
#[pyfunction]
fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
// Use `downcast` instead of `extract` as turning `PyDowncastError` into `PyErr` is quite costly.
if let Ok(list) = value.downcast::<PyList>() {
frobnicate_list(list)
} else if let Ok(vec) = value.extract::<Vec<Bound<'_, PyAny>>>() {
frobnicate_vec(vec)
} else {
Err(PyTypeError::new_err("Cannot frobnicate that type."))
}
}
Access to Bound implies access to GIL token
Calling Python::with_gil
is effectively a no-op when the GIL is already held, but checking that this is the case still has a cost. If an existing GIL token can not be accessed, for example when implementing a pre-existing trait, but a GIL-bound reference is available, this cost can be avoided by exploiting that access to GIL-bound reference gives zero-cost access to a GIL token via Bound::py
.
For example, instead of writing
#![allow(dead_code)]
use pyo3::prelude::*;
use pyo3::types::PyList;
struct Foo(Py<PyList>);
struct FooBound<'py>(Bound<'py, PyList>);
impl PartialEq<Foo> for FooBound<'_> {
fn eq(&self, other: &Foo) -> bool {
Python::with_gil(|py| {
let len = other.0.bind(py).len();
self.0.len() == len
})
}
}
use the more efficient
#![allow(dead_code)]
use pyo3::prelude::*;
use pyo3::types::PyList;
struct Foo(Py<PyList>);
struct FooBound<'py>(Bound<'py, PyList>);
impl PartialEq<Foo> for FooBound<'_> {
fn eq(&self, other: &Foo) -> bool {
// Access to `&Bound<'py, PyAny>` implies access to `Python<'py>`.
let py = self.0.py();
let len = other.0.bind(py).len();
self.0.len() == len
}
}
Disable the global reference pool
PyO3 uses global mutable state to keep track of deferred reference count updates implied by impl<T> Drop for Py<T>
being called without the GIL being held. The necessary synchronization to obtain and apply these reference count updates when PyO3-based code next acquires the GIL is somewhat expensive and can become a significant part of the cost of crossing the Python-Rust boundary.
This functionality can be avoided by setting the pyo3_disable_reference_pool
conditional compilation flag. This removes the global reference pool and the associated costs completely. However, it does not remove the Drop
implementation for Py<T>
which is necessary to interoperate with existing Rust code written without PyO3-based code in mind. To stay compatible with the wider Rust ecosystem in these cases, we keep the implementation but abort when Drop
is called without the GIL being held. If pyo3_leak_on_drop_without_reference_pool
is additionally enabled, objects dropped without the GIL being held will be leaked instead which is always sound but might have determinal effects like resource exhaustion in the long term.
This limitation is important to keep in mind when this setting is used, especially when embedding Python code into a Rust application as it is quite easy to accidentally drop a Py<T>
(or types containing it like PyErr
, PyBackedStr
or PyBackedBytes
) returned from Python::with_gil
without making sure to re-acquire the GIL beforehand. For example, the following code
use pyo3::prelude::*;
use pyo3::types::PyList;
let numbers: Py<PyList> = Python::with_gil(|py| PyList::empty_bound(py).unbind());
Python::with_gil(|py| {
numbers.bind(py).append(23).unwrap();
});
Python::with_gil(|py| {
numbers.bind(py).append(42).unwrap();
});
will abort if the list not explicitly disposed via
use pyo3::prelude::*;
use pyo3::types::PyList;
let numbers: Py<PyList> = Python::with_gil(|py| PyList::empty_bound(py).unbind());
Python::with_gil(|py| {
numbers.bind(py).append(23).unwrap();
});
Python::with_gil(|py| {
numbers.bind(py).append(42).unwrap();
});
Python::with_gil(move |py| {
drop(numbers);
});