diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-04-11-59-33.gh-issue-158592._BpoeY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-04-11-59-33.gh-issue-158592._BpoeY.rst new file mode 100644 index 00000000000000..d937a81cc4630a --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-04-11-59-33.gh-issue-158592._BpoeY.rst @@ -0,0 +1,2 @@ +Skip the array reallocation when shrinking a small :class:`list`. Patch by +Donghee Na. diff --git a/Objects/listobject.c b/Objects/listobject.c index 962ed70aed6b8d..0d28eabb07f60f 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -91,6 +91,10 @@ ensure_shared_on_resize(PyListObject *self) #endif } +#define LIST_SMALL_ALLOCATED 32 + +static int py_list_resize(PyListObject *self, Py_ssize_t newsize); + /* Ensure ob_item has room for at least newsize elements, and set * ob_size to newsize. If newsize > ob_size on entry, the content * of the new slots at exit is undefined heap trash; it's the caller's @@ -99,22 +103,37 @@ ensure_shared_on_resize(PyListObject *self) * Failure is impossible if newsize <= self.allocated on entry. * Note that self->ob_item may change, and even if newsize is less * than ob_size on entry. + * + * Always inlining list_resize() makes the fast path a few instructions + * in each caller instead of a function call. */ -static int +static inline Py_ALWAYS_INLINE int list_resize(PyListObject *self, Py_ssize_t newsize) { - size_t new_allocated, target_bytes; Py_ssize_t allocated = self->allocated; /* Bypass realloc() when a previous overallocation is large enough to accommodate the newsize. If the newsize falls lower than half the allocated size, then proceed with the realloc() to shrink the list. + gh-158592: do not shrink a small list, the realloc() cost is bigger + than the memory we get back. */ - if (allocated >= newsize && newsize >= (allocated >> 1)) { + if (allocated >= newsize + && (newsize >= (allocated >> 1) || allocated <= LIST_SMALL_ALLOCATED)) + { assert(self->ob_item != NULL || newsize == 0); Py_SET_SIZE(self, newsize); return 0; } + return py_list_resize(self, newsize); +} + +/* Slow path of list_resize(): allocate or reallocate ob_item. */ +static int +py_list_resize(PyListObject *self, Py_ssize_t newsize) +{ + size_t new_allocated, target_bytes; + Py_ssize_t allocated = self->allocated; /* This over-allocates proportional to the list size, making room * for additional growth. The over-allocation is mild, but is @@ -136,6 +155,8 @@ list_resize(PyListObject *self, Py_ssize_t newsize) if (newsize == 0) new_allocated = 0; + assert(newsize > allocated || new_allocated < (size_t)allocated); + ensure_shared_on_resize(self); #ifdef Py_GIL_DISABLED @@ -1148,7 +1169,7 @@ list_ass_item_lock_held(PyListObject *a, Py_ssize_t i, PyObject *v) for (Py_ssize_t idx = i; idx < size - 1; idx++) { FT_ATOMIC_STORE_PTR_RELEASE(a->ob_item[idx], a->ob_item[idx + 1]); } - Py_SET_SIZE(a, size - 1); + list_resize(a, size - 1); // NB: shrinking a list can't fail } else { FT_ATOMIC_STORE_PTR_RELEASE(a->ob_item[i], Py_NewRef(v));