From bbad8e9d3929e13f90d2c6d5fe262e5fbbb814af Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Sun, 4 Oct 2026 12:01:55 +0900 Subject: [PATCH 1/8] gh-158592: Skip reallocation when shrinking a small list --- .../2026-10-04-11-59-33.gh-issue-158592._BpoeY.rst | 2 ++ Objects/listobject.c | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-04-11-59-33.gh-issue-158592._BpoeY.rst 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 000000000000000..d937a81cc4630a0 --- /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 81eb3e1102159b5..3e03c6c2db646e5 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -91,6 +91,8 @@ ensure_shared_on_resize(PyListObject *self) #endif } +#define LIST_SMALL_ALLOCATED 16 + /* 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 @@ -136,6 +138,15 @@ list_resize(PyListObject *self, Py_ssize_t newsize) if (newsize == 0) new_allocated = 0; + // gh-158602: when shrinking, do not reallocate the array of a small list. + if (newsize < allocated) { + if (allocated <= LIST_SMALL_ALLOCATED) { + Py_SET_SIZE(self, newsize); + return 0; + } + assert(new_allocated < (size_t)allocated); + } + ensure_shared_on_resize(self); #ifdef Py_GIL_DISABLED @@ -1148,7 +1159,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)); From 23ad656bfc0cfee7fdef8462ade0aa0f0f5fb271 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Sun, 4 Oct 2026 21:29:06 +0900 Subject: [PATCH 2/8] Address code review --- Objects/listobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index 3e03c6c2db646e5..37e0bfe51fc6473 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -91,7 +91,7 @@ ensure_shared_on_resize(PyListObject *self) #endif } -#define LIST_SMALL_ALLOCATED 16 +#define LIST_SMALL_ALLOCATED 32 /* Ensure ob_item has room for at least newsize elements, and set * ob_size to newsize. If newsize > ob_size on entry, the content From 5cd80dbdf5e21346477606e4fca4cf51aea2f3a8 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Mon, 5 Oct 2026 15:15:33 +0900 Subject: [PATCH 3/8] Address code review --- Objects/listobject.c | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index 37e0bfe51fc6473..3dca725c7936e81 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -111,8 +111,12 @@ list_resize(PyListObject *self, Py_ssize_t newsize) /* 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-158602: 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; @@ -138,14 +142,7 @@ list_resize(PyListObject *self, Py_ssize_t newsize) if (newsize == 0) new_allocated = 0; - // gh-158602: when shrinking, do not reallocate the array of a small list. - if (newsize < allocated) { - if (allocated <= LIST_SMALL_ALLOCATED) { - Py_SET_SIZE(self, newsize); - return 0; - } - assert(new_allocated < (size_t)allocated); - } + assert(newsize > allocated || new_allocated < (size_t)allocated); ensure_shared_on_resize(self); From 89b411f379adf2bd0f20d46c9e7ce82df7dee4f8 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Mon, 5 Oct 2026 15:35:45 +0900 Subject: [PATCH 4/8] Address code review --- Objects/listobject.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index 3dca725c7936e81..47c8c81d454f6a9 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -93,6 +93,8 @@ ensure_shared_on_resize(PyListObject *self) #define LIST_SMALL_ALLOCATED 32 +Py_NO_INLINE 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 @@ -102,10 +104,9 @@ ensure_shared_on_resize(PyListObject *self) * Note that self->ob_item may change, and even if newsize is less * than ob_size on entry. */ -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 @@ -121,6 +122,14 @@ list_resize(PyListObject *self, Py_ssize_t newsize) Py_SET_SIZE(self, newsize); return 0; } + return py_list_resize(self, newsize); +} + +Py_NO_INLINE 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 From a4f3cff91c872fd033ce013665750b91ab460c9f Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Mon, 5 Oct 2026 16:03:03 +0900 Subject: [PATCH 5/8] Apply suggestion from @picnixz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- .../2026-10-04-11-59-33.gh-issue-158592._BpoeY.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 index d937a81cc4630a0..8cc288198895c32 100644 --- 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 @@ -1,2 +1,4 @@ -Skip the array reallocation when shrinking a small :class:`list`. Patch by -Donghee Na. +Skip the array reallocation when shrinking a small :class:`list`- +When deleting an item from a list via :keyword:`del`, lists are +also now correctly reallocated unless small, thereby avoiding +holding memory for too long. Patch by Donghee Na. From dd855fdf74dbf62e60d2212a88b2d1f1a0717204 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Mon, 5 Oct 2026 16:13:52 +0900 Subject: [PATCH 6/8] Revert "Apply suggestion from @picnixz" This reverts commit 134f2fdb685e7439ad302a6bfb800345200d600e. --- .../2026-10-04-11-59-33.gh-issue-158592._BpoeY.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 index 8cc288198895c32..d937a81cc4630a0 100644 --- 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 @@ -1,4 +1,2 @@ -Skip the array reallocation when shrinking a small :class:`list`- -When deleting an item from a list via :keyword:`del`, lists are -also now correctly reallocated unless small, thereby avoiding -holding memory for too long. Patch by Donghee Na. +Skip the array reallocation when shrinking a small :class:`list`. Patch by +Donghee Na. From 3404db85f81154e0c2f8d5d9178c944a5a2706b5 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Wed, 7 Oct 2026 00:57:50 +0900 Subject: [PATCH 7/8] Address code review --- Objects/listobject.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index 47c8c81d454f6a9..66269bee8add74d 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -93,7 +93,7 @@ ensure_shared_on_resize(PyListObject *self) #define LIST_SMALL_ALLOCATED 32 -Py_NO_INLINE static int py_list_resize(PyListObject *self, Py_ssize_t newsize); +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 @@ -103,6 +103,10 @@ Py_NO_INLINE static int py_list_resize(PyListObject *self, Py_ssize_t newsize); * 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. + * + * list_resize() only handles the fast path that does not need realloc(). + * Always inlining this function makes the fast path a few instructions + * in each caller instead of a function call. */ static inline Py_ALWAYS_INLINE int list_resize(PyListObject *self, Py_ssize_t newsize) @@ -112,7 +116,7 @@ list_resize(PyListObject *self, Py_ssize_t newsize) /* 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-158602: do not shrink a small list, the realloc() cost is bigger + gh-158592: do not shrink a small list, the realloc() cost is bigger than the memory we get back. */ if (allocated >= newsize @@ -125,7 +129,8 @@ list_resize(PyListObject *self, Py_ssize_t newsize) return py_list_resize(self, newsize); } -Py_NO_INLINE static int +/* 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; From 6cd6e2a4e2bd671546345ad9dd16a9a2491b7f65 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Wed, 7 Oct 2026 00:59:43 +0900 Subject: [PATCH 8/8] nit --- Objects/listobject.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index 66269bee8add74d..42f855a9f697a32 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -104,8 +104,7 @@ static int py_list_resize(PyListObject *self, Py_ssize_t newsize); * Note that self->ob_item may change, and even if newsize is less * than ob_size on entry. * - * list_resize() only handles the fast path that does not need realloc(). - * Always inlining this function makes the fast path a few instructions + * Always inlining list_resize() makes the fast path a few instructions * in each caller instead of a function call. */ static inline Py_ALWAYS_INLINE int