Skip to content

Commit 21fb9dc

Browse files
authored
gh-146527: Heap-allocate gc_stats to avoid bloating PyInterpreterState (#148057)
The gc_stats struct contains ring buffers of gc_generation_stats entries (11 young + 3×2 old on default builds). Embedding it inline in _gc_runtime_state, which is itself inline in PyInterpreterState, pushed fields like _gil.locked and threads.head to offsets beyond what out-of-process profilers and debuggers can reasonably read in a single buffer (e.g. offset 9384 for _gil.locked vs an 8 KiB read buffer). Heap-allocate generation_stats via PyMem_RawCalloc in _PyGC_Init and free it in _PyGC_Fini. This shrinks PyInterpreterState by ~1.6 KiB and keeps the GIL, thread-list, and other frequently-inspected fields at stable, low offsets.
1 parent b1d2d98 commit 21fb9dc

File tree

4 files changed

+24
-10
lines changed

4 files changed

+24
-10
lines changed

Include/internal/pycore_interp_structs.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ struct _gc_runtime_state {
248248
struct gc_generation old[2];
249249
/* a permanent generation which won't be collected */
250250
struct gc_generation permanent_generation;
251-
struct gc_stats generation_stats;
251+
struct gc_stats *generation_stats;
252252
/* true if we are currently running the collector */
253253
int collecting;
254254
// The frame that started the current collection. It might be NULL even when

Modules/gcmodule.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,9 +347,9 @@ gc_get_stats_impl(PyObject *module)
347347
/* To get consistent values despite allocations while constructing
348348
the result list, we use a snapshot of the running stats. */
349349
GCState *gcstate = get_gc_state();
350-
stats[0] = gcstate->generation_stats.young.items[gcstate->generation_stats.young.index];
351-
stats[1] = gcstate->generation_stats.old[0].items[gcstate->generation_stats.old[0].index];
352-
stats[2] = gcstate->generation_stats.old[1].items[gcstate->generation_stats.old[1].index];
350+
stats[0] = gcstate->generation_stats->young.items[gcstate->generation_stats->young.index];
351+
stats[1] = gcstate->generation_stats->old[0].items[gcstate->generation_stats->old[0].index];
352+
stats[2] = gcstate->generation_stats->old[1].items[gcstate->generation_stats->old[1].index];
353353

354354
PyObject *result = PyList_New(0);
355355
if (result == NULL)

Python/gc.c

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ _PyGC_Init(PyInterpreterState *interp)
177177
{
178178
GCState *gcstate = &interp->gc;
179179

180+
gcstate->generation_stats = PyMem_RawCalloc(1, sizeof(struct gc_stats));
181+
if (gcstate->generation_stats == NULL) {
182+
return _PyStatus_NO_MEMORY();
183+
}
184+
180185
gcstate->garbage = PyList_New(0);
181186
if (gcstate->garbage == NULL) {
182187
return _PyStatus_NO_MEMORY();
@@ -1398,13 +1403,13 @@ static struct gc_generation_stats *
13981403
gc_get_stats(GCState *gcstate, int gen)
13991404
{
14001405
if (gen == 0) {
1401-
struct gc_young_stats_buffer *buffer = &gcstate->generation_stats.young;
1406+
struct gc_young_stats_buffer *buffer = &gcstate->generation_stats->young;
14021407
buffer->index = (buffer->index + 1) % GC_YOUNG_STATS_SIZE;
14031408
struct gc_generation_stats *stats = &buffer->items[buffer->index];
14041409
return stats;
14051410
}
14061411
else {
1407-
struct gc_old_stats_buffer *buffer = &gcstate->generation_stats.old[gen - 1];
1412+
struct gc_old_stats_buffer *buffer = &gcstate->generation_stats->old[gen - 1];
14081413
buffer->index = (buffer->index + 1) % GC_OLD_STATS_SIZE;
14091414
struct gc_generation_stats *stats = &buffer->items[buffer->index];
14101415
return stats;
@@ -1415,12 +1420,12 @@ static struct gc_generation_stats *
14151420
gc_get_prev_stats(GCState *gcstate, int gen)
14161421
{
14171422
if (gen == 0) {
1418-
struct gc_young_stats_buffer *buffer = &gcstate->generation_stats.young;
1423+
struct gc_young_stats_buffer *buffer = &gcstate->generation_stats->young;
14191424
struct gc_generation_stats *stats = &buffer->items[buffer->index];
14201425
return stats;
14211426
}
14221427
else {
1423-
struct gc_old_stats_buffer *buffer = &gcstate->generation_stats.old[gen - 1];
1428+
struct gc_old_stats_buffer *buffer = &gcstate->generation_stats->old[gen - 1];
14241429
struct gc_generation_stats *stats = &buffer->items[buffer->index];
14251430
return stats;
14261431
}
@@ -2299,6 +2304,8 @@ _PyGC_Fini(PyInterpreterState *interp)
22992304
GCState *gcstate = &interp->gc;
23002305
Py_CLEAR(gcstate->garbage);
23012306
Py_CLEAR(gcstate->callbacks);
2307+
PyMem_RawFree(gcstate->generation_stats);
2308+
gcstate->generation_stats = NULL;
23022309

23032310
/* Prevent a subtle bug that affects sub-interpreters that use basic
23042311
* single-phase init extensions (m_size == -1). Those extensions cause objects

Python/gc_free_threading.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,11 @@ _PyGC_Init(PyInterpreterState *interp)
16981698
{
16991699
GCState *gcstate = &interp->gc;
17001700

1701+
gcstate->generation_stats = PyMem_RawCalloc(1, sizeof(struct gc_stats));
1702+
if (gcstate->generation_stats == NULL) {
1703+
return _PyStatus_NO_MEMORY();
1704+
}
1705+
17011706
gcstate->garbage = PyList_New(0);
17021707
if (gcstate->garbage == NULL) {
17031708
return _PyStatus_NO_MEMORY();
@@ -2387,12 +2392,12 @@ static struct gc_generation_stats *
23872392
get_stats(GCState *gcstate, int gen)
23882393
{
23892394
if (gen == 0) {
2390-
struct gc_young_stats_buffer *buffer = &gcstate->generation_stats.young;
2395+
struct gc_young_stats_buffer *buffer = &gcstate->generation_stats->young;
23912396
struct gc_generation_stats *stats = &buffer->items[buffer->index];
23922397
return stats;
23932398
}
23942399
else {
2395-
struct gc_old_stats_buffer *buffer = &gcstate->generation_stats.old[gen - 1];
2400+
struct gc_old_stats_buffer *buffer = &gcstate->generation_stats->old[gen - 1];
23962401
struct gc_generation_stats *stats = &buffer->items[buffer->index];
23972402
return stats;
23982403
}
@@ -2831,6 +2836,8 @@ _PyGC_Fini(PyInterpreterState *interp)
28312836
GCState *gcstate = &interp->gc;
28322837
Py_CLEAR(gcstate->garbage);
28332838
Py_CLEAR(gcstate->callbacks);
2839+
PyMem_RawFree(gcstate->generation_stats);
2840+
gcstate->generation_stats = NULL;
28342841

28352842
/* We expect that none of this interpreters objects are shared
28362843
with other interpreters.

0 commit comments

Comments
 (0)