Add intern! macro which can be used to amortize the cost of creating Python objects by storing them inside a GILOnceCell.

This commit is contained in:
Adam Reichold 2022-04-03 12:27:34 +02:00
parent d3dcbd72ba
commit db109867d3
5 changed files with 82 additions and 0 deletions

View File

@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added methods on `InterpreterConfig` to run Python scripts using the configured executable. [#2092](https://github.com/PyO3/pyo3/pull/2092)
- Added FFI definitions for `PyType_FromModuleAndSpec`, `PyType_GetModule`, `PyType_GetModuleState` and `PyModule_AddType`. [#2250](https://github.com/PyO3/pyo3/pull/2250)
- Add `PyString::intern` to enable usage of the Python's built-in string interning. [#2268](https://github.com/PyO3/pyo3/pull/2268)
- Add `intern!` macro which can be used to amortize the cost of creating Python objects by storing them inside a `GILOnceCell`. [#2269](https://github.com/PyO3/pyo3/pull/2269)
### Changed

View File

@ -142,6 +142,10 @@ harness = false
name = "bench_tuple"
harness = false
[[bench]]
name = "bench_intern"
harness = false
[workspace]
members = [
"pyo3-ffi",

33
benches/bench_intern.rs Normal file
View File

@ -0,0 +1,33 @@
use criterion::{criterion_group, criterion_main, Bencher, Criterion};
use pyo3::prelude::*;
use pyo3::{intern, prepare_freethreaded_python};
fn getattr_direct(b: &mut Bencher<'_>) {
prepare_freethreaded_python();
Python::with_gil(|py| {
let sys = py.import("sys").unwrap();
b.iter(|| sys.getattr("version").unwrap());
});
}
fn getattr_intern(b: &mut Bencher<'_>) {
prepare_freethreaded_python();
Python::with_gil(|py| {
let sys = py.import("sys").unwrap();
b.iter(|| sys.getattr(intern!(py, "version")).unwrap());
});
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("getattr_direct", getattr_direct);
c.bench_function("getattr_intern", getattr_intern);
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@ -375,6 +375,7 @@ pub mod impl_;
mod instance;
pub mod marker;
pub mod marshal;
#[macro_use]
pub mod once_cell;
pub mod panic;
pub mod prelude;

View File

@ -101,3 +101,46 @@ impl<T> GILOnceCell<T> {
Ok(())
}
}
/// Converts `value` into a Python object and stores it in static storage. The same Python object
/// is returned on each invocation.
///
/// Because it is stored in a static, this object's destructor will not run.
///
/// # Example: Using `intern!` to avoid needlessly recreating the same object
///
/// ```
/// use pyo3::intern;
/// # use pyo3::{pyfunction, types::PyDict, PyResult, Python};
///
/// #[pyfunction]
/// fn create_dict(py: Python<'_>) -> PyResult<&PyDict> {
/// let dict = PyDict::new(py);
/// // 👇 A new `PyString` is created
/// // for every call of this function
/// dict.set_item("foo", 42)?;
/// Ok(dict)
/// }
///
/// #[pyfunction]
/// fn create_dict_faster(py: Python<'_>) -> PyResult<&PyDict> {
/// let dict = PyDict::new(py);
/// // 👇 A `PyString` is created once and reused
/// // for the lifetime of the program.
/// dict.set_item(intern!(py, "foo"), 42)?;
/// Ok(dict)
/// }
/// ```
#[macro_export]
macro_rules! intern {
($py: expr, $value: expr) => {{
static INTERNED: $crate::once_cell::GILOnceCell<$crate::PyObject> =
$crate::once_cell::GILOnceCell::new();
INTERNED
.get_or_init($py, || {
$crate::conversion::ToPyObject::to_object($value, $py)
})
.as_ref($py)
}};
}