Skip to content

Commit ddd9f10

Browse files
committed
Use a blocking approach to reduce allocation overhead of the lock-free reference pool
1 parent e2ce401 commit ddd9f10

File tree

1 file changed

+55
-10
lines changed

1 file changed

+55
-10
lines changed

src/gil.rs

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ impl Drop for GILGuard {
282282
#[cfg(not(pyo3_disable_reference_pool))]
283283
/// Thread-safe storage for objects which were dec_ref while the GIL was not held.
284284
struct PendingDecRef {
285-
obj: NonNull<ffi::PyObject>,
285+
objs: [NonNull<ffi::PyObject>; 32],
286+
cnt: usize,
286287
next: *mut PendingDecRef,
287288
}
288289

@@ -291,17 +292,57 @@ static POOL: AtomicPtr<PendingDecRef> = AtomicPtr::new(null_mut());
291292

292293
#[cfg(not(pyo3_disable_reference_pool))]
293294
fn enqueue_decref(obj: NonNull<ffi::PyObject>) {
295+
let old_top = POOL.swap(null_mut(), Ordering::AcqRel);
296+
297+
let mut curr = old_top;
298+
299+
while !curr.is_null() {
300+
// SAFETY: We have exclusive ownership of the full stack starting at `old_top`.
301+
let val = unsafe { &mut *curr };
302+
303+
if val.cnt < val.objs.len() {
304+
val.objs[val.cnt] = obj;
305+
val.cnt += 1;
306+
307+
let mut last_next = &mut val.next;
308+
309+
while !last_next.is_null() {
310+
last_next = unsafe { &mut (**last_next).next };
311+
}
312+
313+
POOL.fetch_update(Ordering::AcqRel, Ordering::Relaxed, |new_top| {
314+
*last_next = new_top;
315+
Some(old_top)
316+
})
317+
.unwrap();
318+
return;
319+
}
320+
321+
curr = val.next;
322+
}
323+
324+
// There is no existing stack or it has no unused capacity remaining,
325+
// hence we allocate a new block and prepend it locally before publishing.
326+
let mut objs = [NonNull::dangling(); 32];
327+
objs[0] = obj;
328+
294329
let val = PendingDecRef {
295-
obj,
296-
next: null_mut(),
330+
objs,
331+
cnt: 1,
332+
next: old_top,
297333
};
298334

299335
let top = Box::into_raw(Box::new(val));
300336

301-
let next = unsafe { &mut (*top).next };
337+
// SAFETY: We just allocated `top` using `Box::new`.
338+
let mut last_next = unsafe { &mut (*top).next };
339+
340+
while !last_next.is_null() {
341+
last_next = unsafe { &mut (**last_next).next };
342+
}
302343

303344
POOL.fetch_update(Ordering::AcqRel, Ordering::Relaxed, |new_top| {
304-
*next = new_top;
345+
*last_next = new_top;
305346
Some(top)
306347
})
307348
.unwrap();
@@ -314,7 +355,9 @@ fn update_counts(_py: Python<'_>) {
314355
// SAFETY: Was enqueued using `Box::into_raw`.
315356
let val = unsafe { Box::from_raw(top) };
316357

317-
unsafe { ffi::Py_DECREF(val.obj.as_ptr()) };
358+
for obj in &val.objs[..val.cnt] {
359+
unsafe { ffi::Py_DECREF(obj.as_ptr()) };
360+
}
318361

319362
top = val.next;
320363
}
@@ -581,11 +624,13 @@ mod tests {
581624
// SAFETY: Was enqueued using `Box::into_raw`.
582625
let val = unsafe { Box::from_raw(top) };
583626

584-
if val.obj.as_ptr() == obj.as_ptr() {
585-
found = true;
586-
}
627+
for obj1 in &val.objs[..val.cnt] {
628+
if obj1.as_ptr() == obj.as_ptr() {
629+
found = true;
630+
}
587631

588-
unsafe { ffi::Py_DECREF(val.obj.as_ptr()) };
632+
unsafe { ffi::Py_DECREF(obj1.as_ptr()) };
633+
}
589634

590635
top = val.next;
591636
}

0 commit comments

Comments
 (0)