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.
This commit is contained in:
Pablo Galindo Salgado 2026-04-04 18:42:30 +01:00 committed by GitHub
parent b1d2d9829c
commit 21fb9dc71d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 24 additions and 10 deletions

View file

@ -177,6 +177,11 @@ _PyGC_Init(PyInterpreterState *interp)
{
GCState *gcstate = &interp->gc;
gcstate->generation_stats = PyMem_RawCalloc(1, sizeof(struct gc_stats));
if (gcstate->generation_stats == NULL) {
return _PyStatus_NO_MEMORY();
}
gcstate->garbage = PyList_New(0);
if (gcstate->garbage == NULL) {
return _PyStatus_NO_MEMORY();
@ -1398,13 +1403,13 @@ static struct gc_generation_stats *
gc_get_stats(GCState *gcstate, int gen)
{
if (gen == 0) {
struct gc_young_stats_buffer *buffer = &gcstate->generation_stats.young;
struct gc_young_stats_buffer *buffer = &gcstate->generation_stats->young;
buffer->index = (buffer->index + 1) % GC_YOUNG_STATS_SIZE;
struct gc_generation_stats *stats = &buffer->items[buffer->index];
return stats;
}
else {
struct gc_old_stats_buffer *buffer = &gcstate->generation_stats.old[gen - 1];
struct gc_old_stats_buffer *buffer = &gcstate->generation_stats->old[gen - 1];
buffer->index = (buffer->index + 1) % GC_OLD_STATS_SIZE;
struct gc_generation_stats *stats = &buffer->items[buffer->index];
return stats;
@ -1415,12 +1420,12 @@ static struct gc_generation_stats *
gc_get_prev_stats(GCState *gcstate, int gen)
{
if (gen == 0) {
struct gc_young_stats_buffer *buffer = &gcstate->generation_stats.young;
struct gc_young_stats_buffer *buffer = &gcstate->generation_stats->young;
struct gc_generation_stats *stats = &buffer->items[buffer->index];
return stats;
}
else {
struct gc_old_stats_buffer *buffer = &gcstate->generation_stats.old[gen - 1];
struct gc_old_stats_buffer *buffer = &gcstate->generation_stats->old[gen - 1];
struct gc_generation_stats *stats = &buffer->items[buffer->index];
return stats;
}
@ -2299,6 +2304,8 @@ _PyGC_Fini(PyInterpreterState *interp)
GCState *gcstate = &interp->gc;
Py_CLEAR(gcstate->garbage);
Py_CLEAR(gcstate->callbacks);
PyMem_RawFree(gcstate->generation_stats);
gcstate->generation_stats = NULL;
/* Prevent a subtle bug that affects sub-interpreters that use basic
* single-phase init extensions (m_size == -1). Those extensions cause objects

View file

@ -1698,6 +1698,11 @@ _PyGC_Init(PyInterpreterState *interp)
{
GCState *gcstate = &interp->gc;
gcstate->generation_stats = PyMem_RawCalloc(1, sizeof(struct gc_stats));
if (gcstate->generation_stats == NULL) {
return _PyStatus_NO_MEMORY();
}
gcstate->garbage = PyList_New(0);
if (gcstate->garbage == NULL) {
return _PyStatus_NO_MEMORY();
@ -2387,12 +2392,12 @@ static struct gc_generation_stats *
get_stats(GCState *gcstate, int gen)
{
if (gen == 0) {
struct gc_young_stats_buffer *buffer = &gcstate->generation_stats.young;
struct gc_young_stats_buffer *buffer = &gcstate->generation_stats->young;
struct gc_generation_stats *stats = &buffer->items[buffer->index];
return stats;
}
else {
struct gc_old_stats_buffer *buffer = &gcstate->generation_stats.old[gen - 1];
struct gc_old_stats_buffer *buffer = &gcstate->generation_stats->old[gen - 1];
struct gc_generation_stats *stats = &buffer->items[buffer->index];
return stats;
}
@ -2831,6 +2836,8 @@ _PyGC_Fini(PyInterpreterState *interp)
GCState *gcstate = &interp->gc;
Py_CLEAR(gcstate->garbage);
Py_CLEAR(gcstate->callbacks);
PyMem_RawFree(gcstate->generation_stats);
gcstate->generation_stats = NULL;
/* We expect that none of this interpreters objects are shared
with other interpreters.