Skip to content

Commit b788b9b

Browse files
authored
introduce fast path to LazyTypeObject::get_or_init (#5324)
* introduce fast path to `LazyTypeObject::get_or_init` * fix changelog name * reduce duplication * fixup benchmark * fixup clippy * fixup doc
1 parent 15045db commit b788b9b

File tree

7 files changed

+61
-62
lines changed

7 files changed

+61
-62
lines changed

guide/src/class.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1385,7 +1385,12 @@ unsafe impl pyo3::type_object::PyTypeInfo for MyClass {
13851385
#[inline]
13861386
fn type_object_raw(py: pyo3::Python<'_>) -> *mut pyo3::ffi::PyTypeObject {
13871387
<Self as pyo3::impl_::pyclass::PyClassImpl>::lazy_type_object()
1388-
.get_or_init(py)
1388+
.get_or_try_init(py)
1389+
.unwrap_or_else(|e| pyo3::impl_::pyclass::type_object_init_failed(
1390+
py,
1391+
e,
1392+
<Self as pyo3::type_object::PyTypeInfo>::NAME
1393+
))
13891394
.as_type_ptr()
13901395
}
13911396
}

newsfragments/5324.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add fast-path to `PyTypeInfo::type_object` for `#[pyclass]` types.

pyo3-benches/benches/bench_pyclass.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub fn first_time_init(b: &mut Bencher<'_>) {
3232
// This is using an undocumented internal PyO3 API to measure pyclass performance; please
3333
// don't use this in your own code!
3434
let ty = LazyTypeObject::<MyClass>::new();
35-
ty.get_or_init(py);
35+
ty.get_or_try_init(py).unwrap();
3636
});
3737
});
3838
}

pyo3-macros-backend/src/pyclass.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1845,7 +1845,12 @@ fn impl_pytypeinfo(cls: &syn::Ident, attr: &PyClassArgs, ctx: &Ctx) -> TokenStre
18451845
fn type_object_raw(py: #pyo3_path::Python<'_>) -> *mut #pyo3_path::ffi::PyTypeObject {
18461846
use #pyo3_path::prelude::PyTypeMethods;
18471847
<#cls as #pyo3_path::impl_::pyclass::PyClassImpl>::lazy_type_object()
1848-
.get_or_init(py)
1848+
.get_or_try_init(py)
1849+
.unwrap_or_else(|e| #pyo3_path::impl_::pyclass::type_object_init_failed(
1850+
py,
1851+
e,
1852+
<Self as #pyo3_path::type_object::PyTypeInfo>::NAME
1853+
))
18491854
.as_type_ptr()
18501855
}
18511856
}

src/impl_/pyclass.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ mod lazy_type_object;
2929
mod probes;
3030

3131
pub use assertions::*;
32-
pub use lazy_type_object::LazyTypeObject;
32+
pub use lazy_type_object::{type_object_init_failed, LazyTypeObject};
3333
pub use probes::*;
3434

3535
/// Gets the offset of the dictionary from the start of the object in bytes.

src/impl_/pyclass/lazy_type_object.rs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ struct LazyTypeObjectInner {
3232
// Threads which have begun initialization of the `tp_dict`. Used for
3333
// reentrant initialization detection.
3434
initializing_threads: Mutex<Vec<ThreadId>>,
35-
tp_dict_filled: GILOnceCell<()>,
35+
fully_initialized_type: GILOnceCell<Py<PyType>>,
3636
}
3737

3838
impl<T> LazyTypeObject<T> {
@@ -43,7 +43,7 @@ impl<T> LazyTypeObject<T> {
4343
LazyTypeObjectInner {
4444
value: GILOnceCell::new(),
4545
initializing_threads: Mutex::new(Vec::new()),
46-
tp_dict_filled: GILOnceCell::new(),
46+
fully_initialized_type: GILOnceCell::new(),
4747
},
4848
PhantomData,
4949
)
@@ -52,15 +52,18 @@ impl<T> LazyTypeObject<T> {
5252

5353
impl<T: PyClass> LazyTypeObject<T> {
5454
/// Gets the type object contained by this `LazyTypeObject`, initializing it if needed.
55-
pub fn get_or_init<'py>(&self, py: Python<'py>) -> &Bound<'py, PyType> {
56-
self.get_or_try_init(py).unwrap_or_else(|err| {
57-
err.print(py);
58-
panic!("failed to create type object for {}", T::NAME)
59-
})
55+
#[inline]
56+
pub fn get_or_try_init<'py>(&self, py: Python<'py>) -> PyResult<&Bound<'py, PyType>> {
57+
if let Some(type_object) = self.0.fully_initialized_type.get(py) {
58+
// Fast path
59+
return Ok(type_object.bind(py));
60+
}
61+
62+
self.try_init(py)
6063
}
6164

62-
/// Fallible version of the above.
63-
pub(crate) fn get_or_try_init<'py>(&self, py: Python<'py>) -> PyResult<&Bound<'py, PyType>> {
65+
#[cold]
66+
fn try_init<'py>(&self, py: Python<'py>) -> PyResult<&Bound<'py, PyType>> {
6467
self.0
6568
.get_or_try_init(py, create_type_object::<T>, T::NAME, T::items_iter())
6669
}
@@ -116,7 +119,7 @@ impl LazyTypeObjectInner {
116119
// `tp_dict`, it can still request the type object through `get_or_init`,
117120
// but the `tp_dict` may appear empty of course.
118121

119-
if self.tp_dict_filled.get(py).is_some() {
122+
if self.fully_initialized_type.get(py).is_some() {
120123
// `tp_dict` is already filled: ok.
121124
return Ok(());
122125
}
@@ -184,8 +187,8 @@ impl LazyTypeObjectInner {
184187

185188
// Now we hold the GIL and we can assume it won't be released until we
186189
// return from the function.
187-
let result = self.tp_dict_filled.get_or_try_init(py, move || {
188-
let result = initialize_tp_dict(py, type_object.as_ptr(), items);
190+
let result = self.fully_initialized_type.get_or_try_init(py, move || {
191+
initialize_tp_dict(py, type_object.as_ptr(), items)?;
189192
#[cfg(Py_3_14)]
190193
if is_immutable_type {
191194
// freeze immutable types after __dict__ is initialized
@@ -216,13 +219,13 @@ impl LazyTypeObjectInner {
216219
self.initializing_threads.lock().unwrap()
217220
};
218221
threads.clear();
219-
result
222+
Ok(type_object.clone().unbind())
220223
});
221224

222225
if let Err(err) = result {
223226
return Err(wrap_in_runtime_error(
224227
py,
225-
err.clone_ref(py),
228+
err,
226229
format!("An error occurred while initializing `{name}.__dict__`"),
227230
));
228231
}
@@ -249,6 +252,13 @@ fn initialize_tp_dict(
249252
// This is necessary for making static `LazyTypeObject`s
250253
unsafe impl<T> Sync for LazyTypeObject<T> {}
251254

255+
/// Used in the macro-expanded implementation of `type_object_raw` for `#[pyclass]` types
256+
#[cold]
257+
pub fn type_object_init_failed(py: Python<'_>, err: PyErr, type_name: &str) -> ! {
258+
err.write_unraisable(py, None);
259+
panic!("failed to create type object for `{type_name}`")
260+
}
261+
252262
#[cold]
253263
fn wrap_in_runtime_error(py: Python<'_>, err: PyErr, message: String) -> PyErr {
254264
let runtime_err = PyRuntimeError::new_err(message);

tests/test_class_attributes.rs

Lines changed: 22 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -152,40 +152,10 @@ fn recursive_class_attributes() {
152152
}
153153

154154
#[test]
155+
#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] // sys.unraisablehook not available until Python 3.8
155156
fn test_fallible_class_attribute() {
156-
use pyo3::{exceptions::PyValueError, types::PyString};
157-
158-
struct CaptureStdErr<'py> {
159-
oldstderr: Bound<'py, PyAny>,
160-
string_io: Bound<'py, PyAny>,
161-
}
162-
163-
impl<'py> CaptureStdErr<'py> {
164-
fn new(py: Python<'py>) -> PyResult<Self> {
165-
let sys = py.import("sys")?;
166-
let oldstderr = sys.getattr("stderr")?;
167-
let string_io = py.import("io")?.getattr("StringIO")?.call0()?;
168-
sys.setattr("stderr", &string_io)?;
169-
Ok(Self {
170-
oldstderr,
171-
string_io,
172-
})
173-
}
174-
175-
fn reset(self) -> PyResult<String> {
176-
let py = self.string_io.py();
177-
let payload = self
178-
.string_io
179-
.getattr("getvalue")?
180-
.call0()?
181-
.cast::<PyString>()?
182-
.to_cow()?
183-
.into_owned();
184-
let sys = py.import("sys")?;
185-
sys.setattr("stderr", self.oldstderr)?;
186-
Ok(payload)
187-
}
188-
}
157+
use common::UnraisableCapture;
158+
use pyo3::exceptions::PyValueError;
189159

190160
#[pyclass]
191161
struct BrokenClass;
@@ -199,21 +169,29 @@ fn test_fallible_class_attribute() {
199169
}
200170

201171
Python::attach(|py| {
202-
let stderr = CaptureStdErr::new(py).unwrap();
172+
let capture = UnraisableCapture::install(py);
203173
assert!(std::panic::catch_unwind(|| py.get_type::<BrokenClass>()).is_err());
204-
assert_eq!(
205-
stderr.reset().unwrap().trim(),
206-
"\
207-
ValueError: failed to create class attribute
208-
209-
The above exception was the direct cause of the following exception:
210174

211-
RuntimeError: An error occurred while initializing `BrokenClass.fails_to_init`
175+
let (err, object) = capture.borrow_mut(py).capture.take().unwrap();
176+
assert!(object.is_none(py));
212177

213-
The above exception was the direct cause of the following exception:
178+
assert_eq!(
179+
err.to_string(),
180+
"RuntimeError: An error occurred while initializing class BrokenClass"
181+
);
182+
let cause = err.cause(py).unwrap();
183+
assert_eq!(
184+
cause.to_string(),
185+
"RuntimeError: An error occurred while initializing `BrokenClass.fails_to_init`"
186+
);
187+
let cause = cause.cause(py).unwrap();
188+
assert_eq!(
189+
cause.to_string(),
190+
"ValueError: failed to create class attribute"
191+
);
192+
assert!(cause.cause(py).is_none());
214193

215-
RuntimeError: An error occurred while initializing class BrokenClass"
216-
)
194+
capture.borrow_mut(py).uninstall(py);
217195
});
218196
}
219197

0 commit comments

Comments
 (0)