Skip to content

Commit 2d2b37f

Browse files
authored
[3.14] gh-158592: Skip reallocation when shrinking a small list (GH-158787) (gh-159002)
1 parent 6490ca6 commit 2d2b37f

3 files changed

Lines changed: 28 additions & 4 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``del list[index]`` now triggers shrinking the list's internal array.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Skip the array reallocation when shrinking a small :class:`list`. Patch by
2+
Donghee Na.

‎Objects/listobject.c‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ ensure_shared_on_resize(PyListObject *self)
9191
#endif
9292
}
9393

94+
#define LIST_SMALL_ALLOCATED 32
95+
96+
static int py_list_resize(PyListObject *self, Py_ssize_t newsize);
97+
9498
/* Ensure ob_item has room for at least newsize elements, and set
9599
* ob_size to newsize. If newsize > ob_size on entry, the content
96100
* of the new slots at exit is undefined heap trash; it's the caller's
@@ -103,22 +107,37 @@ ensure_shared_on_resize(PyListObject *self)
103107
* imagine a realloc implementation where it wouldn't be true).
104108
* Note that self->ob_item may change, and even if newsize is less
105109
* than ob_size on entry.
110+
*
111+
* Always inlining list_resize() makes the fast path a few instructions
112+
* in each caller instead of a function call.
106113
*/
107-
static int
114+
static inline Py_ALWAYS_INLINE int
108115
list_resize(PyListObject *self, Py_ssize_t newsize)
109116
{
110-
size_t new_allocated, target_bytes;
111117
Py_ssize_t allocated = self->allocated;
112118

113119
/* Bypass realloc() when a previous overallocation is large enough
114120
to accommodate the newsize. If the newsize falls lower than half
115121
the allocated size, then proceed with the realloc() to shrink the list.
122+
gh-158592: do not shrink a small list, the realloc() cost is bigger
123+
than the memory we get back.
116124
*/
117-
if (allocated >= newsize && newsize >= (allocated >> 1)) {
125+
if (allocated >= newsize
126+
&& (newsize >= (allocated >> 1) || allocated <= LIST_SMALL_ALLOCATED))
127+
{
118128
assert(self->ob_item != NULL || newsize == 0);
119129
Py_SET_SIZE(self, newsize);
120130
return 0;
121131
}
132+
return py_list_resize(self, newsize);
133+
}
134+
135+
/* Slow path of list_resize(): allocate or reallocate ob_item. */
136+
static int
137+
py_list_resize(PyListObject *self, Py_ssize_t newsize)
138+
{
139+
size_t new_allocated, target_bytes;
140+
Py_ssize_t allocated = self->allocated;
122141

123142
/* This over-allocates proportional to the list size, making room
124143
* for additional growth. The over-allocation is mild, but is
@@ -140,6 +159,8 @@ list_resize(PyListObject *self, Py_ssize_t newsize)
140159
if (newsize == 0)
141160
new_allocated = 0;
142161

162+
assert(newsize > allocated || new_allocated < (size_t)allocated);
163+
143164
ensure_shared_on_resize(self);
144165

145166
#ifdef Py_GIL_DISABLED
@@ -1093,7 +1114,7 @@ list_ass_item_lock_held(PyListObject *a, Py_ssize_t i, PyObject *v)
10931114
for (Py_ssize_t idx = i; idx < size - 1; idx++) {
10941115
FT_ATOMIC_STORE_PTR_RELAXED(a->ob_item[idx], a->ob_item[idx + 1]);
10951116
}
1096-
Py_SET_SIZE(a, size - 1);
1117+
list_resize(a, size - 1); // NB: shrinking a list can't fail
10971118
}
10981119
else {
10991120
FT_ATOMIC_STORE_PTR_RELEASE(a->ob_item[i], Py_NewRef(v));

0 commit comments

Comments
 (0)