Skip to content

Commit bea3a41

Browse files
Merge pull request #3 from faster-cpython/experimental-gc-fix
Experimental gc fix
2 parents 07682c9 + 615a96e commit bea3a41

File tree

4 files changed

+59
-35
lines changed

4 files changed

+59
-35
lines changed

Include/internal/pycore_gc.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,8 @@ struct _gc_runtime_state {
328328
Py_ssize_t heap_size;
329329
Py_ssize_t work_to_do;
330330
/* Which of the old spaces is the visited space */
331-
int visited_space;
331+
uint8_t visited_space;
332+
uint8_t scan_reachable;
332333

333334
#ifdef Py_GIL_DISABLED
334335
/* This is the number of objects that survived the last full
@@ -351,6 +352,7 @@ struct _gc_runtime_state {
351352
<0: suppressed; don't immortalize objects */
352353
int immortalize;
353354
#endif
355+
Py_ssize_t prior_heap_size;
354356
};
355357

356358
#ifdef Py_GIL_DISABLED

Lib/test/test_gc.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import _testcapi
2323
from _testcapi import with_tp_del
2424
from _testcapi import ContainerNoGC
25+
import _testinternalcapi
2526
except ImportError:
2627
_testcapi = None
2728
def with_tp_del(cls):
@@ -1101,32 +1102,21 @@ def make_ll(depth):
11011102
return head
11021103

11031104
head = make_ll(1000)
1104-
count = 1000
1105-
1106-
# There will be some objects we aren't counting,
1107-
# e.g. the gc stats dicts. This test checks
1108-
# that the counts don't grow, so we try to
1109-
# correct for the uncounted objects
1110-
# This is just an estimate.
1111-
CORRECTION = 20
11121105

11131106
enabled = gc.isenabled()
11141107
gc.enable()
11151108
olds = []
1116-
for i in range(20_000):
1109+
gc.collect()
1110+
baseline_live = _testinternalcapi.get_heap_size()
1111+
iterations = 200_000 if support.is_resource_enabled('cpu') else 20_000
1112+
for i in range(iterations):
11171113
newhead = make_ll(20)
1118-
count += 20
11191114
newhead.surprise = head
11201115
olds.append(newhead)
11211116
if len(olds) == 20:
1122-
stats = gc.get_stats()
1123-
young = stats[0]
1124-
incremental = stats[1]
1125-
old = stats[2]
1126-
collected = young['collected'] + incremental['collected'] + old['collected']
1127-
count += CORRECTION
1128-
live = count - collected
1129-
self.assertLess(live, 25000)
1117+
live = _testinternalcapi.get_heap_size()
1118+
print(i, live, baseline_live)
1119+
self.assertLess(live, baseline_live*2)
11301120
del olds[:]
11311121
if not enabled:
11321122
gc.disable()

Modules/_testinternalcapi.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2048,6 +2048,11 @@ identify_type_slot_wrappers(PyObject *self, PyObject *Py_UNUSED(ignored))
20482048
return _PyType_GetSlotWrapperNames();
20492049
}
20502050

2051+
static PyObject *
2052+
get_heap_size(PyObject *self, PyObject *Py_UNUSED(ignored))
2053+
{
2054+
return PyLong_FromSsize_t(PyInterpreterState_Get()->gc.heap_size);
2055+
}
20512056

20522057
static PyMethodDef module_functions[] = {
20532058
{"get_configs", get_configs, METH_NOARGS},
@@ -2145,6 +2150,7 @@ static PyMethodDef module_functions[] = {
21452150
GH_119213_GETARGS_METHODDEF
21462151
{"get_static_builtin_types", get_static_builtin_types, METH_NOARGS},
21472152
{"identify_type_slot_wrappers", identify_type_slot_wrappers, METH_NOARGS},
2153+
{"get_heap_size", get_heap_size, METH_NOARGS, NULL},
21482154
{NULL, NULL} /* sentinel */
21492155
};
21502156

Python/gc.c

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ _PyGC_Init(PyInterpreterState *interp)
182182
return _PyStatus_NO_MEMORY();
183183
}
184184
gcstate->heap_size = 0;
185+
gcstate->prior_heap_size = 0;
185186

186187
return _PyStatus_OK();
187188
}
@@ -1278,19 +1279,16 @@ gc_list_set_space(PyGC_Head *list, int space)
12781279
* the incremental collector must progress through the old
12791280
* space faster than objects are added to the old space.
12801281
*
1281-
* Each young or incremental collection adds a number of
1282-
* objects, S (for survivors) to the old space, and
1283-
* incremental collectors scan I objects from the old space.
1284-
* I > S must be true. We also want I > S * N to be where
1285-
* N > 1. Higher values of N mean that the old space is
1286-
* scanned more rapidly.
1287-
* The default incremental threshold of 10 translates to
1288-
* N == 1.4 (1 + 4/threshold)
1282+
* To do this we maintain a prior heap size, so the
1283+
* change in heap size can easily be computed.
1284+
*
1285+
* Each increment scans twice the delta (if increasing)
1286+
* plus half the size of the young generation.
12891287
*/
12901288

1291-
/* Divide by 10, so that the default incremental threshold of 10
1292-
* scans objects at 1% of the heap size */
1293-
#define SCAN_RATE_DIVISOR 10
1289+
/* Multiply by 5, so that the default incremental threshold of 10
1290+
* scans objects at half the rate of the young generation */
1291+
#define SCAN_RATE_MULTIPLIER 5
12941292

12951293
static void
12961294
add_stats(GCState *gcstate, int gen, struct gc_collection_stats *stats)
@@ -1344,7 +1342,6 @@ gc_collect_young(PyThreadState *tstate,
13441342
if (scale_factor < 1) {
13451343
scale_factor = 1;
13461344
}
1347-
gcstate->work_to_do += gcstate->heap_size / SCAN_RATE_DIVISOR / scale_factor;
13481345
add_stats(gcstate, 0, stats);
13491346
}
13501347

@@ -1431,8 +1428,30 @@ completed_cycle(GCState *gcstate)
14311428
gc = next;
14321429
}
14331430
gcstate->work_to_do = 0;
1431+
gcstate->scan_reachable = 1;
1432+
}
1433+
1434+
1435+
static void
1436+
gc_mark_reachable(PyThreadState *tstate)
1437+
{
1438+
GCState *gcstate = &tstate->interp->gc;
1439+
PyGC_Head *visited = &gcstate->old[gcstate->visited_space].head;
1440+
PyObject *sysdict = tstate->interp->sysdict;
1441+
PyObject *sysmod = PyDict_GetItemString(sysdict, "modules");
1442+
if (sysmod == NULL) {
1443+
return;
1444+
}
1445+
PyGC_Head reachable;
1446+
gc_list_init(&reachable);
1447+
PyGC_Head *gc = _Py_AS_GC(sysmod);
1448+
gc_list_move(gc, &reachable);
1449+
gc_set_old_space(gc, gcstate->visited_space);
1450+
gcstate->work_to_do -= expand_region_transitively_reachable(&reachable, gc, gcstate);
1451+
gc_list_merge(&reachable, visited);
14341452
}
14351453

1454+
14361455
static void
14371456
gc_collect_increment(PyThreadState *tstate, struct gc_collection_stats *stats)
14381457
{
@@ -1442,13 +1461,14 @@ gc_collect_increment(PyThreadState *tstate, struct gc_collection_stats *stats)
14421461
PyGC_Head *visited = &gcstate->old[gcstate->visited_space].head;
14431462
PyGC_Head increment;
14441463
gc_list_init(&increment);
1464+
if (gcstate->scan_reachable) {
1465+
gc_mark_reachable(tstate);
1466+
gcstate->scan_reachable = 0;
1467+
}
14451468
Py_ssize_t scale_factor = gcstate->old[0].threshold;
14461469
if (scale_factor < 1) {
14471470
scale_factor = 1;
14481471
}
1449-
gc_list_merge(&gcstate->young.head, &increment);
1450-
gcstate->young.count = 0;
1451-
gc_list_validate_space(&increment, gcstate->visited_space);
14521472
Py_ssize_t increment_size = 0;
14531473
while (increment_size < gcstate->work_to_do) {
14541474
if (gc_list_is_empty(not_visited)) {
@@ -1467,7 +1487,12 @@ gc_collect_increment(PyThreadState *tstate, struct gc_collection_stats *stats)
14671487
gc_list_validate_space(&survivors, gcstate->visited_space);
14681488
gc_list_merge(&survivors, visited);
14691489
assert(gc_list_is_empty(&increment));
1470-
gcstate->work_to_do += gcstate->heap_size / SCAN_RATE_DIVISOR / scale_factor;
1490+
Py_ssize_t delta = (gcstate->heap_size - gcstate->prior_heap_size)*3;
1491+
delta += gcstate->young.threshold * SCAN_RATE_MULTIPLIER / scale_factor;
1492+
if (delta > 0) {
1493+
gcstate->work_to_do += delta;
1494+
}
1495+
gcstate->prior_heap_size = gcstate->heap_size;
14711496
gcstate->work_to_do -= increment_size;
14721497

14731498
validate_old(gcstate);
@@ -1856,6 +1881,7 @@ _PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason)
18561881
gc_collect_young(tstate, &stats);
18571882
break;
18581883
case 1:
1884+
gc_collect_young(tstate, &stats);
18591885
gc_collect_increment(tstate, &stats);
18601886
break;
18611887
case 2:

0 commit comments

Comments
 (0)