Skip to content

Commit c5f9001

Browse files
authored
Remove deferred reference count increments and make the global reference pool optional (#4095)
* Add feature controlling the global reference pool to enable avoiding its overhead. * Document reference-pool feature in the performance guide. * Invert semantics of feature to disable reference pool so the new behaviour becomes opt-in * Remove delayed reference count increments as we cannot prevent reference count errors as long as these are available * Adjust tests to be compatible with disable-reference-pool feature * Adjust tests to be compatible with py-clone feature * Adjust the GIL benchmark to the updated reference pool semantics. * Further extend and clarify the documentation of the py-clone and disable-reference-pool features * Replace disable-reference-pool feature by pyo3_disable_reference_pool conditional compilation flag Such a flag is harder to use and thereby also harder to abuse. This seems appropriate as this is purely a performance-oriented change which show only be enabled by leaf crates and brings with it additional highly implicit sources of process aborts. * Add pyo3_leak_on_drop_without_reference_pool to turn aborts into leaks when the global reference pool is disabled and the GIL is not held
1 parent 033caa8 commit c5f9001

32 files changed

+226
-240
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ auto-initialize = []
108108
# Allows use of the deprecated "GIL Refs" APIs.
109109
gil-refs = []
110110

111+
# Enables `Clone`ing references to Python objects `Py<T>` which panics if the GIL is not held.
112+
py-clone = []
113+
111114
# Optimizes PyObject to Vec conversion and so on.
112115
nightly = []
113116

@@ -129,6 +132,7 @@ full = [
129132
"num-bigint",
130133
"num-complex",
131134
"num-rational",
135+
"py-clone",
132136
"rust_decimal",
133137
"serde",
134138
"smallvec",

examples/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ pyo3 = { path = "..", features = ["auto-initialize", "extension-module"] }
1010
[[example]]
1111
name = "decorator"
1212
path = "decorator/src/lib.rs"
13-
crate_type = ["cdylib"]
13+
crate-type = ["cdylib"]
1414
doc-scrape-examples = true

guide/src/class.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ fn return_myclass() -> Py<MyClass> {
249249

250250
let obj = return_myclass();
251251

252-
Python::with_gil(|py| {
252+
Python::with_gil(move |py| {
253253
let bound = obj.bind(py); // Py<MyClass>::bind returns &Bound<'py, MyClass>
254254
let obj_ref = bound.borrow(); // Get PyRef<T>
255255
assert_eq!(obj_ref.num, 1);
@@ -280,6 +280,8 @@ let py_counter: Py<FrozenCounter> = Python::with_gil(|py| {
280280
});
281281

282282
py_counter.get().value.fetch_add(1, Ordering::Relaxed);
283+
284+
Python::with_gil(move |_py| drop(py_counter));
283285
```
284286

285287
Frozen classes are likely to become the default thereby guiding the PyO3 ecosystem towards a more deliberate application of interior mutability. Eventually, this should enable further optimizations of PyO3's internals and avoid downstream code paying the cost of interior mutability when it is not actually required.

guide/src/faq.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,10 @@ If you don't want that cloning to happen, a workaround is to allocate the field
127127
```rust
128128
# use pyo3::prelude::*;
129129
#[pyclass]
130-
#[derive(Clone)]
131130
struct Inner {/* fields omitted */}
132131

133132
#[pyclass]
134133
struct Outer {
135-
#[pyo3(get)]
136134
inner: Py<Inner>,
137135
}
138136

@@ -144,6 +142,11 @@ impl Outer {
144142
inner: Py::new(py, Inner {})?,
145143
})
146144
}
145+
146+
#[getter]
147+
fn inner(&self, py: Python<'_>) -> Py<Inner> {
148+
self.inner.clone_ref(py)
149+
}
147150
}
148151
```
149152
This time `a` and `b` *are* the same object:

guide/src/features.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ This feature is a backwards-compatibility feature to allow continued use of the
7575

7676
This feature and the APIs it enables is expected to be removed in a future PyO3 version.
7777

78+
### `py-clone`
79+
80+
This feature was introduced to ease migration. It was found that delayed reference counts cannot be made sound and hence `Clon`ing an instance of `Py<T>` must panic without the GIL being held. To avoid migrations introducing new panics without warning, the `Clone` implementation itself is now gated behind this feature.
81+
82+
### `pyo3_disable_reference_pool`
83+
84+
This is a performance-oriented conditional compilation flag, e.g. [set via `$RUSTFLAGS`][set-configuration-options], which disabled the global reference pool and the assocaited overhead for the crossing the Python-Rust boundary. However, if enabled, `Drop`ping an instance of `Py<T>` without the GIL being held will abort the process.
85+
7886
### `macros`
7987

8088
This feature enables a dependency on the `pyo3-macros` crate, which provides the procedural macros portion of PyO3's API:
@@ -195,3 +203,5 @@ struct User {
195203
### `smallvec`
196204

197205
Adds a dependency on [smallvec](https://docs.rs/smallvec) and enables conversions into its [`SmallVec`](https://docs.rs/smallvec/latest/smallvec/struct.SmallVec.html) type.
206+
207+
[set-configuration-options]: https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options

guide/src/memory.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,8 @@ This example wasn't very interesting. We could have just used a GIL-bound
212212
we are *not* holding the GIL?
213213

214214
```rust
215-
# #![allow(unused_imports)]
215+
# #![allow(unused_imports, dead_code)]
216+
# #[cfg(not(pyo3_disable_reference_pool))] {
216217
# use pyo3::prelude::*;
217218
# use pyo3::types::PyString;
218219
# fn main() -> PyResult<()> {
@@ -239,12 +240,14 @@ Python::with_gil(|py|
239240
# }
240241
# Ok(())
241242
# }
243+
# }
242244
```
243245

244246
When `hello` is dropped *nothing* happens to the pointed-to memory on Python's
245247
heap because nothing _can_ happen if we're not holding the GIL. Fortunately,
246-
the memory isn't leaked. PyO3 keeps track of the memory internally and will
247-
release it the next time we acquire the GIL.
248+
the memory isn't leaked. If the `pyo3_disable_reference_pool` conditional compilation flag
249+
is not enabled, PyO3 keeps track of the memory internally and will release it
250+
the next time we acquire the GIL.
248251

249252
We can avoid the delay in releasing memory if we are careful to drop the
250253
`Py<Any>` while the GIL is held.

guide/src/migration.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,16 @@ fn increment(x: u64, amount: Option<u64>) -> u64 {
3535
x + amount.unwrap_or(1)
3636
}
3737
```
38+
</details>
39+
40+
### `Py::clone` is now gated behind the `py-clone` feature
41+
<details open>
42+
<summary><small>Click to expand</small></summary>
43+
If you rely on `impl<T> Clone for Py<T>` to fulfil trait requirements imposed by existing Rust code written without PyO3-based code in mind, the newly introduced feature `py-clone` must be enabled.
3844

45+
However, take care to note that the behaviour is different from previous versions. If `Clone` was called without the GIL being held, we tried to delay the application of these reference count increments until PyO3-based code would re-acquire it. This turned out to be impossible to implement in a sound manner and hence was removed. Now, if `Clone` is called without the GIL being held, we panic instead for which calling code might not be prepared.
46+
47+
Related to this, we also added a `pyo3_disable_reference_pool` conditional compilation flag which removes the infrastructure necessary to apply delayed reference count decrements implied by `impl<T> Drop for Py<T>`. They do not appear to be a soundness hazard as they should lead to memory leaks in the worst case. However, the global synchronization adds significant overhead to cross the Python-Rust boundary. Enabling this feature will remove these costs and make the `Drop` implementation abort the process if called without the GIL being held instead.
3948
</details>
4049

4150
## from 0.20.* to 0.21
@@ -676,7 +685,7 @@ drop(second);
676685

677686
The replacement is [`Python::with_gil`](https://docs.rs/pyo3/0.18.3/pyo3/marker/struct.Python.html#method.with_gil) which is more cumbersome but enforces the proper nesting by design, e.g.
678687

679-
```rust
688+
```rust,ignore
680689
# #![allow(dead_code)]
681690
# use pyo3::prelude::*;
682691
@@ -701,7 +710,7 @@ let second = Python::with_gil(|py| Object::new(py));
701710
drop(first);
702711
drop(second);
703712
704-
// Or it ensure releasing the inner lock before the outer one.
713+
// Or it ensures releasing the inner lock before the outer one.
705714
Python::with_gil(|py| {
706715
let first = Object::new(py);
707716
let second = Python::with_gil(|py| Object::new(py));

guide/src/performance.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,47 @@ impl PartialEq<Foo> for FooBound<'_> {
9696
}
9797
}
9898
```
99+
100+
## Disable the global reference pool
101+
102+
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.
103+
104+
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.
105+
106+
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
107+
108+
```rust,ignore
109+
# use pyo3::prelude::*;
110+
# use pyo3::types::PyList;
111+
let numbers: Py<PyList> = Python::with_gil(|py| PyList::empty_bound(py).unbind());
112+
113+
Python::with_gil(|py| {
114+
numbers.bind(py).append(23).unwrap();
115+
});
116+
117+
Python::with_gil(|py| {
118+
numbers.bind(py).append(42).unwrap();
119+
});
120+
```
121+
122+
will abort if the list not explicitly disposed via
123+
124+
```rust
125+
# use pyo3::prelude::*;
126+
# use pyo3::types::PyList;
127+
let numbers: Py<PyList> = Python::with_gil(|py| PyList::empty_bound(py).unbind());
128+
129+
Python::with_gil(|py| {
130+
numbers.bind(py).append(23).unwrap();
131+
});
132+
133+
Python::with_gil(|py| {
134+
numbers.bind(py).append(42).unwrap();
135+
});
136+
137+
Python::with_gil(move |py| {
138+
drop(numbers);
139+
});
140+
```
141+
142+
[conditional-compilation]: https://doc.rust-lang.org/reference/conditional-compilation.html

newsfragments/4095.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `pyo3_disable_reference_pool` conditional compilation flag to avoid the overhead of the global reference pool at the cost of known limitations as explained in the performance section of the guide.

newsfragments/4095.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`Clone`ing pointers into the Python heap has been moved behind the `py-clone` feature, as it must panic without the GIL being held as a soundness fix.

0 commit comments

Comments
 (0)