GH-139951: Fix major GC performance regression (GH-140262) · python/cpython@0c01090 · GitHub
Skip to content

Commit 0c01090

Browse files
authored
GH-139951: Fix major GC performance regression (GH-140262)
* Count number of actually tracked objects, instead of trackable objects. This ensures that untracking tuples has the desired effect of reducing GC overhead * Do not track most untrackable tuples during creation. This prevents large numbers of small tuples causing execessive GCs.
1 parent 4adf08a commit 0c01090

5 files changed

Lines changed: 93 additions & 36 deletions

File tree

Include/internal/pycore_gc.h

Lines changed: 19 additions & 4 deletions

Lib/test/test_gc.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,10 +1473,11 @@ def callback(ignored):
14731473
# The free-threaded build doesn't have multiple generations, so
14741474
# just trigger a GC manually.
14751475
gc.collect()
1476+
assert not detector.gc_happened
14761477
while not detector.gc_happened:
14771478
i += 1
1478-
if i > 10000:
1479-
self.fail("gc didn't happen after 10000 iterations")
1479+
if i > 100000:
1480+
self.fail("gc didn't happen after 100000 iterations")
14801481
self.assertEqual(len(ouch), 0)
14811482
junk.append([]) # this will eventually trigger gc
14821483

@@ -1548,8 +1549,8 @@ def __del__(self):
15481549
gc.collect()
15491550
while not detector.gc_happened:
15501551
i += 1
1551-
if i > 10000:
1552-
self.fail("gc didn't happen after 10000 iterations")
1552+
if i > 50000:
1553+
self.fail("gc didn't happen after 50000 iterations")
15531554
self.assertEqual(len(ouch), 0)
15541555
junk.append([]) # this will eventually trigger gc
15551556

@@ -1566,8 +1567,8 @@ def test_indirect_calls_with_gc_disabled(self):
15661567
detector = GC_Detector()
15671568
while not detector.gc_happened:
15681569
i += 1
1569-
if i > 10000:
1570-
self.fail("gc didn't happen after 10000 iterations")
1570+
if i > 100000:
1571+
self.fail("gc didn't happen after 100000 iterations")
15711572
junk.append([]) # this will eventually trigger gc
15721573

15731574
try:
@@ -1577,11 +1578,11 @@ def test_indirect_calls_with_gc_disabled(self):
15771578
detector = GC_Detector()
15781579
while not detector.gc_happened:
15791580
i += 1
1580-
if i > 10000:
1581+
if i > 100000:
15811582
break
15821583
junk.append([]) # this may eventually trigger gc (if it is enabled)
15831584

1584-
self.assertEqual(i, 10001)
1585+
self.assertEqual(i, 100001)
15851586
finally:
15861587
gc.enable()
15871588

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fixes a regression in GC performance for a growing heap composed mostly of
2+
small tuples.
3+
4+
* Counts number of actually tracked objects, instead of trackable objects.
5+
This ensures that untracking tuples has the desired effect of reducing GC overhead.
6+
* Does not track most untrackable tuples during creation.
7+
This prevents large numbers of small tuples causing excessive GCs.

Objects/tupleobject.c

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,26 @@ _PyTuple_MaybeUntrack(PyObject *op)
156156
_PyObject_GC_UNTRACK(op);
157157
}
158158

159+
/* Fast, but conservative check if an object maybe tracked
160+
May return true for an object that is not tracked,
161+
Will always return true for an object that is tracked.
162+
This is a temporary workaround until _PyObject_GC_IS_TRACKED
163+
becomes fast and safe to call on non-GC objects.
164+
*/
165+
static bool
166+
maybe_tracked(PyObject *ob)
167+
{
168+
return _PyType_IS_GC(Py_TYPE(ob));
169+
}
170+
159171
PyObject *
160172
PyTuple_Pack(Py_ssize_t n, ...)
161173
{
162174
Py_ssize_t i;
163175
PyObject *o;
164176
PyObject **items;
165177
va_list vargs;
178+
bool track = false;
166179

167180
if (n == 0) {
168181
return tuple_get_empty();
@@ -177,10 +190,15 @@ PyTuple_Pack(Py_ssize_t n, ...)
177190
items = result->ob_item;
178191
for (i = 0; i < n; i++) {
179192
o = va_arg(vargs, PyObject *);
193+
if (!track && maybe_tracked(o)) {
194+
track = true;
195+
}
180196
items[i] = Py_NewRef(o);
181197
}
182198
va_end(vargs);
183-
_PyObject_GC_TRACK(result);
199+
if (track) {
200+
_PyObject_GC_TRACK(result);
201+
}
184202
return (PyObject *)result;
185203
}
186204

@@ -377,11 +395,17 @@ PyTuple_FromArray(PyObject *const *src, Py_ssize_t n)
377395
return NULL;
378396
}
379397
PyObject **dst = tuple->ob_item;
398+
bool track = false;
380399
for (Py_ssize_t i = 0; i < n; i++) {
381400
PyObject *item = src[i];
401+
if (!track && maybe_tracked(item)) {
402+
track = true;
403+
}
382404
dst[i] = Py_NewRef(item);
383405
}
384-
_PyObject_GC_TRACK(tuple);
406+
if (track) {
407+
_PyObject_GC_TRACK(tuple);
408+
}
385409
return (PyObject *)tuple;
386410
}
387411

@@ -396,10 +420,17 @@ _PyTuple_FromStackRefStealOnSuccess(const _PyStackRef *src, Py_ssize_t n)
396420
return NULL;
397421
}
398422
PyObject **dst = tuple->ob_item;
423+
bool track = false;
399424
for (Py_ssize_t i = 0; i < n; i++) {
400-
dst[i] = PyStackRef_AsPyObjectSteal(src[i]);
425+
PyObject *item = PyStackRef_AsPyObjectSteal(src[i]);
426+
if (!track && maybe_tracked(item)) {
427+
track = true;
428+
}
429+
dst[i] = item;
430+
}
431+
if (track) {
432+
_PyObject_GC_TRACK(tuple);
401433
}
402-
_PyObject_GC_TRACK(tuple);
403434
return (PyObject *)tuple;
404435
}
405436

Python/gc.c

Lines changed: 23 additions & 20 deletions

0 commit comments

Comments
 (0)