Skip to content

Commit 595b14c

Browse files
committed
Clearer calculation of work to do.
1 parent 1452378 commit 595b14c

File tree

4 files changed

+51
-30
lines changed

4 files changed

+51
-30
lines changed

Include/internal/pycore_gc.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ struct _gc_runtime_state {
330330
/* a list of callbacks to be invoked when collection is performed */
331331
PyObject *callbacks;
332332

333+
Py_ssize_t prior_heap_size;
333334
Py_ssize_t heap_size;
334335
Py_ssize_t work_to_do;
335336
/* Which of the old spaces is the visited space */

Lib/test/test_gc.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ def __new__(cls, *args, **kwargs):
3131
return C
3232
ContainerNoGC = None
3333

34+
try:
35+
import _testinternalcapi
36+
except ImportError:
37+
_testinternalcapi = None
38+
3439
### Support code
3540
###############################################################################
3641

@@ -1092,6 +1097,7 @@ def setUp(self):
10921097
def tearDown(self):
10931098
gc.disable()
10941099

1100+
@unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
10951101
@requires_gil_enabled("Free threading does not support incremental GC")
10961102
# Use small increments to emulate longer running process in a shorter time
10971103
@gc_threshold(200, 10)
@@ -1117,32 +1123,18 @@ def make_ll(depth):
11171123
return head
11181124

11191125
head = make_ll(1000)
1120-
count = 1000
1121-
1122-
# There will be some objects we aren't counting,
1123-
# e.g. the gc stats dicts. This test checks
1124-
# that the counts don't grow, so we try to
1125-
# correct for the uncounted objects
1126-
# This is just an estimate.
1127-
CORRECTION = 20
11281126

11291127
enabled = gc.isenabled()
11301128
gc.enable()
11311129
olds = []
1130+
initial_heap_size = _testinternalcapi.get_heap_size()
11321131
for i in range(20_000):
11331132
newhead = make_ll(20)
1134-
count += 20
11351133
newhead.surprise = head
11361134
olds.append(newhead)
11371135
if len(olds) == 20:
1138-
stats = gc.get_stats()
1139-
young = stats[0]
1140-
incremental = stats[1]
1141-
old = stats[2]
1142-
collected = young['collected'] + incremental['collected'] + old['collected']
1143-
count += CORRECTION
1144-
live = count - collected
1145-
self.assertLess(live, 27000)
1136+
new_objects = _testinternalcapi.get_heap_size() - initial_heap_size
1137+
self.assertLess(new_objects, 27_000)
11461138
del olds[:]
11471139
if not enabled:
11481140
gc.disable()

Modules/_testinternalcapi.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2077,6 +2077,12 @@ has_deferred_refcount(PyObject *self, PyObject *op)
20772077
}
20782078

20792079

2080+
static PyObject *
2081+
get_heap_size(PyObject *self, PyObject *Py_UNUSED(ignored))
2082+
{
2083+
return PyLong_FromInt64(PyInterpreterState_Get()->gc.heap_size);
2084+
}
2085+
20802086
static PyMethodDef module_functions[] = {
20812087
{"get_configs", get_configs, METH_NOARGS},
20822088
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -2174,6 +2180,7 @@ static PyMethodDef module_functions[] = {
21742180
{"get_static_builtin_types", get_static_builtin_types, METH_NOARGS},
21752181
{"identify_type_slot_wrappers", identify_type_slot_wrappers, METH_NOARGS},
21762182
{"has_deferred_refcount", has_deferred_refcount, METH_O},
2183+
{"get_heap_size", get_heap_size, METH_NOARGS},
21772184
{NULL, NULL} /* sentinel */
21782185
};
21792186

Python/gc.c

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ _PyGC_Init(PyInterpreterState *interp)
185185
if (gcstate->callbacks == NULL) {
186186
return _PyStatus_NO_MEMORY();
187187
}
188+
gcstate->prior_heap_size = 0;
188189
gcstate->heap_size = 0;
189190

190191
return _PyStatus_OK();
@@ -1330,16 +1331,11 @@ gc_collect_young(PyThreadState *tstate,
13301331
survivor_count++;
13311332
}
13321333
}
1333-
(void)survivor_count; // Silence compiler warning
13341334
gc_list_merge(&survivors, visited);
13351335
validate_old(gcstate);
13361336
gcstate->young.count = 0;
13371337
gcstate->old[gcstate->visited_space].count++;
1338-
Py_ssize_t scale_factor = gcstate->old[0].threshold;
1339-
if (scale_factor < 1) {
1340-
scale_factor = 1;
1341-
}
1342-
gcstate->work_to_do += gcstate->heap_size / SCAN_RATE_DIVISOR / scale_factor;
1338+
gcstate->work_to_do += survivor_count * 4;
13431339
add_stats(gcstate, 0, stats);
13441340
}
13451341

@@ -1559,13 +1555,43 @@ mark_at_start(PyThreadState *tstate)
15591555
return objects_marked;
15601556
}
15611557

1558+
static Py_ssize_t
1559+
assess_work_to_do(GCState *gcstate)
1560+
{
1561+
/* The amount of work we want to do depends on two things.
1562+
* 1. The number of new objects created
1563+
* 2. The growth in heap size since the last collection
1564+
* 3. The heap size (up to the number of new objects, to avoid quadratic effects)
1565+
*/
1566+
Py_ssize_t scale_factor = gcstate->old[0].threshold;
1567+
if (scale_factor < 2) {
1568+
scale_factor = 2;
1569+
}
1570+
Py_ssize_t new_objects = gcstate->young.count;
1571+
Py_ssize_t growth = gcstate->heap_size - gcstate->prior_heap_size;
1572+
if (growth < 0) {
1573+
growth = 0;
1574+
}
1575+
if (gcstate->heap_size < new_objects * scale_factor) {
1576+
// Small heap: ignore growth
1577+
growth = 0;
1578+
}
1579+
Py_ssize_t heap_fraction = gcstate->heap_size / SCAN_RATE_DIVISOR / scale_factor;
1580+
if (heap_fraction > new_objects) {
1581+
heap_fraction = new_objects;
1582+
}
1583+
gcstate->young.count = 0;
1584+
gcstate->prior_heap_size = gcstate->heap_size;
1585+
return new_objects*2 + growth*2 + heap_fraction;
1586+
}
1587+
15621588
static void
15631589
gc_collect_increment(PyThreadState *tstate, struct gc_collection_stats *stats)
15641590
{
15651591
GC_STAT_ADD(1, collections, 1);
15661592
GCState *gcstate = &tstate->interp->gc;
1567-
gcstate->work_to_do += gcstate->young.count;
1568-
gcstate->young.count = 0;
1593+
1594+
gcstate->work_to_do += assess_work_to_do(gcstate);
15691595
if (gcstate->phase == GC_PHASE_MARK) {
15701596
Py_ssize_t objects_marked = mark_at_start(tstate);
15711597
GC_STAT_ADD(1, objects_transitively_reachable, objects_marked);
@@ -1576,10 +1602,6 @@ gc_collect_increment(PyThreadState *tstate, struct gc_collection_stats *stats)
15761602
PyGC_Head *visited = &gcstate->old[gcstate->visited_space].head;
15771603
PyGC_Head increment;
15781604
gc_list_init(&increment);
1579-
Py_ssize_t scale_factor = gcstate->old[0].threshold;
1580-
if (scale_factor < 1) {
1581-
scale_factor = 1;
1582-
}
15831605
Py_ssize_t objects_marked = mark_stacks(tstate->interp, visited, gcstate->visited_space, false);
15841606
GC_STAT_ADD(1, objects_transitively_reachable, objects_marked);
15851607
gcstate->work_to_do -= objects_marked;
@@ -1607,7 +1629,6 @@ gc_collect_increment(PyThreadState *tstate, struct gc_collection_stats *stats)
16071629
gc_list_validate_space(&survivors, gcstate->visited_space);
16081630
gc_list_merge(&survivors, visited);
16091631
assert(gc_list_is_empty(&increment));
1610-
gcstate->work_to_do += gcstate->heap_size / SCAN_RATE_DIVISOR / scale_factor;
16111632
gcstate->work_to_do -= increment_size;
16121633

16131634
validate_old(gcstate);

0 commit comments

Comments
 (0)