Skip to content

Speed up PyList::new on abi3t for free-threaded interpreters - #6532

Open
ngoldbaum wants to merge 2 commits into
PyO3:mainfrom
ngoldbaum:pylist-new-iterator
Open

ngoldbaum wants to merge 2 commits into
PyO3:mainfrom
ngoldbaum:pylist-new-iterator

Conversation

@ngoldbaum

@ngoldbaum ngoldbaum commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

On free-threaded CPython, abi3t PyList::new calls PyList_SetItem for every item, entering the new list's critical section each time (CPython #158660). For size hints of at least 32, PyList::new instead uses a small C-API iterator with PySequence_List so CPython can preallocate and fill the list under one critical section. Short lists and GIL-enabled interpreters keep the existing path.

At 1,024 existing objects and above, the iterator path is 62–75% faster across macOS, arm64 Linux, Windows, and WSL2. The 32 item cutoff is conservative: iterator setup can offset the savings for short lists.

I had to add a small manually defined static heap type since PyList::new needs to work without pyclass and the macros feature. As far as I can tell this is the first spot that defines a type like this, although we define exceptions and functions manually like this in a few spots. I added a TODO to point to the fact that this is incompatible with subinterpreters.

Here's a plot that summarizes the choices in this PR:

image

The dashed blue points are using the iterator approach below the 32 item cutoff to show that this approach performs badly for small lists and to show why I picked 32. The solid blue curve measures the performance improvement from this patch from 32 items onward. The pink line shows the outer-critical-section proposal in #6478. The yellow line shows an alternate idea based on building a tuple and calling PyList_FromSequence on it. The green PyList_SET_ITEM curve shows the theoretical maximum speedup, if we were able to use APIs outside the stable ABI. All curves use existing objects and are relative to the current abi3t PyList_SetItem path in the same environment.

Comment thread src/types/list.rs
/// placeholder exception after an element conversion error.
type ItemProducer = dyn FnMut() -> *mut ffi::PyObject;

crate::platform::thread_local! {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I looked at storing this in type data instead and it ended up slower because it adds function call overhead

Comment thread src/types/list.rs

// TODO(subinterpreters): Cache the `ListBuildIterator` heap type per interpreter once PyO3
// supports subinterpreters.
fn get_iterator_type(py: Python<'_>) -> &Py<PyType> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it possible to create this type using our marco machinery? This will reduce code complexity significantly.

@bschoenmaeckers

Copy link
Copy Markdown
Member

Nice work! Should we have a benchmark for this that shows the improvement and future performance?

Comment thread src/types/list.rs
Comment on lines +145 to +146
/// `PyType_FromSpec` creates the `pyo3.ListBuildIterator` type without the `macros` feature.
/// A `#[pymethods]` `__next__` trampoline costs more per item than the critical section.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry, I've missed this explanation. Can we improve the trampoline to reduce the cost in any way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The other issue is that pyclass needs the macros feature. It would be nice if this optimization didn't rely on that feature.

@ngoldbaum

Copy link
Copy Markdown
Contributor Author

Should we have a benchmark for this that shows the improvement and future performance?

We could but right now we also don't have any limited API benchmark runs in CI. I think if we had an abi3t run the current benchmarks would show a significant improvement in relevant benchmarks. I'll need to double-check that.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants