diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 2198c3792e9..e75095f4bef 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -24,6 +24,7 @@ - [Debugging](debugging.md) - [Features reference](features.md) - [Memory management](memory.md) +- [Performance](performance.md) - [Advanced topics](advanced.md) - [Building and distribution](building_and_distribution.md) - [Supporting multiple Python versions](building_and_distribution/multiple_python_versions.md) diff --git a/guide/src/performance.md b/guide/src/performance.md new file mode 100644 index 00000000000..23fb59c4e90 --- /dev/null +++ b/guide/src/performance.md @@ -0,0 +1,94 @@ +# 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 `&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. + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +# use pyo3::{exceptions::PyTypeError, types::PyList}; + +fn frobnicate_list(list: &PyList) -> PyResult<&PyAny> { + todo!() +} + +fn frobnicate_vec(vec: Vec<&PyAny>) -> PyResult<&PyAny> { + todo!() +} + +#[pyfunction] +fn frobnicate(value: &PyAny) -> PyResult<&PyAny> { + if let Ok(list) = value.extract::<&PyList>() { + frobnicate_list(list) + } else if let Ok(vec) = value.extract::>() { + frobnicate_vec(vec) + } else { + Err(PyTypeError::new_err("Cannot frobnicate that type.")) + } +} +``` + +This suboptimal as the `FromPyObject` trait requires `extract` to have a `Result` 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. + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +# use pyo3::{exceptions::PyTypeError, types::PyList}; +# fn frobnicate_list(list: &PyList) -> PyResult<&PyAny> { todo!() } +# fn frobnicate_vec(vec: Vec<&PyAny>) -> PyResult<&PyAny> { todo!() } +# +#[pyfunction] +fn frobnicate(value: &PyAny) -> PyResult<&PyAny> { + // Use `downcast` instead of `extract` as turning `PyDowncastError` into `PyErr` is quite costly. + if let Ok(list) = value.downcast::() { + frobnicate_list(list) + } else if let Ok(vec) = value.extract::>() { + frobnicate_vec(vec) + } else { + Err(PyTypeError::new_err("Cannot frobnicate that type.")) + } +} +``` + +## Access to GIL-bound reference 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 `PyAny::py`. + +For example, instead of writing + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +# use pyo3::types::PyList; + +struct Foo(Py); + +struct FooRef<'a>(&'a PyList); + +impl PartialEq for FooRef<'_> { + fn eq(&self, other: &Foo) -> bool { + Python::with_gil(|py| self.0.len() == other.0.as_ref(py).len()) + } +} +``` + +use more efficient + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +# use pyo3::types::PyList; +# struct Foo(Py); +# struct FooRef<'a>(&'a PyList); +# +impl PartialEq for FooRef<'_> { + fn eq(&self, other: &Foo) -> bool { + // Access to `&'a PyAny` implies access to `Python<'a>`. + let py = self.0.py(); + self.0.len() == other.0.as_ref(py).len() + } +} +``` diff --git a/newsfragments/3304.added.md b/newsfragments/3304.added.md new file mode 100644 index 00000000000..f3b62eba58e --- /dev/null +++ b/newsfragments/3304.added.md @@ -0,0 +1 @@ +Added a "performance" section to the guide collecting performance-related tricks and problems. diff --git a/src/lib.rs b/src/lib.rs index 1209347ac7a..5987c666d71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -496,6 +496,7 @@ pub mod doc_test { "guide/src/migration.md" => guide_migration_md, "guide/src/module.md" => guide_module_md, "guide/src/parallelism.md" => guide_parallelism_md, + "guide/src/performance.md" => guide_performance_md, "guide/src/python_from_rust.md" => guide_python_from_rust_md, "guide/src/python_typing_hints.md" => guide_python_typing_hints_md, "guide/src/rust_cpython.md" => guide_rust_cpython_md,