Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance on pointer drop #851

Merged
merged 1 commit into from
Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## Unreleased

### Changed

* `PyObject` and `Py<T>` reference counts are now decremented sooner after `drop()`. [#851](https://github.com/PyO3/pyo3/pull/851)
* When the GIL is held, the refcount is now decreased immediately on drop. (Previously would wait until just before releasing the GIL.)
* When the GIL is not held, the refcount is now decreased when the GIL is next acquired. (Previously would wait until next time the GIL was released.)

### Added

* `FromPyObject` implementations for `HashSet` and `BTreeSet`. [#842](https://github.com/PyO3/pyo3/pull/842)
Expand Down
16 changes: 16 additions & 0 deletions benches/bench_pyobject.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![feature(test)]

extern crate test;
use pyo3::prelude::*;
use test::Bencher;

#[bench]
fn drop_many_objects(b: &mut Bencher) {
let gil = Python::acquire_gil();
let py = gil.python();
b.iter(|| {
for _ in 0..1000 {
std::mem::drop(py.None());
}
});
}
1 change: 1 addition & 0 deletions src/ffi/pystate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ extern "C" {
#[cfg_attr(PyPy, link_name = "PyPyGILState_Release")]
pub fn PyGILState_Release(arg1: PyGILState_STATE);
pub fn PyGILState_GetThisThreadState() -> *mut PyThreadState;
pub fn PyGILState_Check() -> c_int;
}

#[inline]
Expand Down
127 changes: 118 additions & 9 deletions src/gil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,33 @@
//! Interaction with python's global interpreter lock

use crate::{ffi, internal_tricks::Unsendable, PyAny, Python};
use std::cell::Cell;
use std::ptr::NonNull;
use std::{any, sync};

static START: sync::Once = sync::Once::new();
static START_PYO3: sync::Once = sync::Once::new();

thread_local! {
/// This is a internal counter in pyo3 monitoring whether this thread has the GIL.
///
/// It will be incremented whenever a GIL-holding RAII struct is created, and decremented
/// whenever they are dropped.
///
/// As a result, if this thread has the GIL, GIL_COUNT is greater than zero.
static GIL_COUNT: Cell<u32> = Cell::new(0);
}

/// Check whether the GIL is acquired.
///
/// Note: This uses pyo3's internal count rather than PyGILState_Check for two reasons:
/// 1) for performance
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
/// 2) PyGILState_Check always returns 1 if the sub-interpreter APIs have ever been called,
/// which could lead to incorrect conclusions that the GIL is held.
fn gil_is_acquired() -> bool {
GIL_COUNT.with(|c| c.get() > 0)
}

/// Prepares the use of Python in a free-threaded context.
///
/// If the Python interpreter is not already initialized, this function
Expand Down Expand Up @@ -110,6 +131,8 @@ impl Drop for GILGuard {
pool.drain(self.python(), self.owned, self.borrowed);
ffi::PyGILState_Release(self.gstate);
}

decrement_gil_count();
}
}

Expand Down Expand Up @@ -177,6 +200,7 @@ pub struct GILPool<'p> {
impl<'p> GILPool<'p> {
#[inline]
pub fn new(py: Python) -> GILPool {
increment_gil_count();
let p: &'static mut ReleasePool = unsafe { &mut *POOL };
GILPool {
py,
Expand All @@ -193,6 +217,7 @@ impl<'p> Drop for GILPool<'p> {
let pool: &'static mut ReleasePool = &mut *POOL;
pool.drain(self.py, self.owned, self.borrowed);
}
decrement_gil_count();
}
}

Expand All @@ -210,7 +235,11 @@ pub unsafe fn register_any<'p, T: 'static>(obj: T) -> &'p T {

pub unsafe fn register_pointer(obj: NonNull<ffi::PyObject>) {
let pool = &mut *POOL;
(**pool.p.lock()).push(obj);
if gil_is_acquired() {
ffi::Py_DECREF(obj.as_ptr())
} else {
(**pool.p.lock()).push(obj);
}
}

pub unsafe fn register_owned(_py: Python, obj: NonNull<ffi::PyObject>) -> &PyAny {
Expand All @@ -233,7 +262,12 @@ impl GILGuard {

unsafe {
let gstate = ffi::PyGILState_Ensure(); // acquire GIL
increment_gil_count();

// Release objects that were dropped since last GIL acquisition
let pool: &'static mut ReleasePool = &mut *POOL;
pool.release_pointers();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to release pointers here?
To reduce memory usage?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two reasons:

  1. Yes, memory usage - we're now releasing the pointers sooner
  2. This (Why object creation is slow in PyO3 and how to enhance it #679 (comment)) suggested to me that it's important in the long run we migrate to releasing objects on GIL acquire, not GIL exit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I understand. Thank you for the reference.


GILGuard {
owned: pool.owned.len(),
borrowed: pool.borrowed.len(),
Expand All @@ -250,6 +284,25 @@ impl GILGuard {
}
}

/// Increment pyo3's internal GIL count - to be called whenever GILPool or GILGuard is created.
#[inline(always)]
fn increment_gil_count() {
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
GIL_COUNT.with(|c| c.set(c.get() + 1))
}

/// Decrement pyo3's internal GIL count - to be called whenever GILPool or GILGuard is dropped.
#[inline(always)]
fn decrement_gil_count() {
GIL_COUNT.with(|c| {
let current = c.get();
debug_assert!(
current > 0,
"Negative GIL count detected. Please report this error to the PyO3 repo as a bug."
);
c.set(current - 1);
})
}

use self::array_list::ArrayList;

mod array_list {
Expand Down Expand Up @@ -308,7 +361,7 @@ mod array_list {

#[cfg(test)]
mod test {
use super::{GILPool, NonNull, ReleasePool, POOL};
use super::{GILPool, NonNull, ReleasePool, GIL_COUNT, POOL};
use crate::object::PyObject;
use crate::AsPyPointer;
use crate::Python;
Expand All @@ -327,7 +380,6 @@ mod test {

#[test]
fn test_owned() {
gil::init_once();
let gil = Python::acquire_gil();
let py = gil.python();
let obj = get_object();
Expand Down Expand Up @@ -356,7 +408,6 @@ mod test {

#[test]
fn test_owned_nested() {
gil::init_once();
let gil = Python::acquire_gil();
let py = gil.python();
let obj = get_object();
Expand Down Expand Up @@ -393,7 +444,6 @@ mod test {
#[test]
fn test_borrowed() {
gil::init_once();

unsafe {
let p: &'static mut ReleasePool = &mut *POOL;

Expand All @@ -420,7 +470,6 @@ mod test {
#[test]
fn test_borrowed_nested() {
gil::init_once();

unsafe {
let p: &'static mut ReleasePool = &mut *POOL;

Expand Down Expand Up @@ -455,8 +504,30 @@ mod test {
}

#[test]
fn test_pyobject_drop() {
gil::init_once();
fn test_pyobject_drop_with_gil_decreases_refcnt() {
let gil = Python::acquire_gil();
let py = gil.python();
let obj = get_object();
// Ensure that obj does not get freed
let _ref = obj.clone_ref(py);
let obj_ptr = obj.as_ptr();

unsafe {
let p: &'static mut ReleasePool = &mut *POOL;

{
assert_eq!(p.owned.len(), 0);
assert_eq!(ffi::Py_REFCNT(obj_ptr), 2);
}

// With the GIL held, obj can be dropped immediately
drop(obj);
assert_eq!(ffi::Py_REFCNT(obj_ptr), 1);
}
}

#[test]
fn test_pyobject_drop_without_gil_doesnt_decrease_refcnt() {
let gil = Python::acquire_gil();
let py = gil.python();
let obj = get_object();
Expand All @@ -471,13 +542,51 @@ mod test {
assert_eq!(p.owned.len(), 0);
assert_eq!(ffi::Py_REFCNT(obj_ptr), 2);
}

// Without the GIL held, obj cannot be dropped until the next GIL acquire
drop(gil);
drop(obj);
assert_eq!(ffi::Py_REFCNT(obj_ptr), 2);

{
// Next time the GIL is acquired, the object is released
let _gil = Python::acquire_gil();
assert_eq!(ffi::Py_REFCNT(obj_ptr), 1);
}
assert_eq!(ffi::Py_REFCNT(obj_ptr), 1);
}
}

#[test]
fn test_gil_counts() {
// Check GILGuard and GILPool both increase counts correctly
let get_gil_count = || GIL_COUNT.with(|c| c.get());

assert_eq!(get_gil_count(), 0);
let gil = Python::acquire_gil();
assert_eq!(get_gil_count(), 1);

let py = gil.python();

assert_eq!(get_gil_count(), 1);
let pool = GILPool::new(py);
assert_eq!(get_gil_count(), 2);

let pool2 = GILPool::new(py);
assert_eq!(get_gil_count(), 3);

drop(pool);
assert_eq!(get_gil_count(), 2);

let gil2 = Python::acquire_gil();
assert_eq!(get_gil_count(), 3);

drop(gil2);
assert_eq!(get_gil_count(), 2);

drop(pool2);
assert_eq!(get_gil_count(), 1);

drop(gil);
assert_eq!(get_gil_count(), 0);
}
}
2 changes: 1 addition & 1 deletion src/pycell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ impl<T: PyClass + fmt::Debug> fmt::Debug for PyCell<T> {
/// # let gil = Python::acquire_gil();
/// # let py = gil.python();
/// # let sub = PyCell::new(py, Child::new()).unwrap();
/// # pyo3::py_run!(py, sub, "assert sub.format() == 'Caterpillar(base: Butterfly, cnt: 4)'");
/// # pyo3::py_run!(py, sub, "assert sub.format() == 'Caterpillar(base: Butterfly, cnt: 3)'");
/// ```
pub struct PyRef<'p, T: PyClass> {
inner: &'p PyCellInner<T>,
Expand Down