]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-146527: Heap-allocate gc_stats to avoid bloating PyInterpreterState (#148057)
authorPablo Galindo Salgado <Pablogsal@gmail.com>
Sat, 4 Apr 2026 17:42:30 +0000 (18:42 +0100)
committerGitHub <noreply@github.com>
Sat, 4 Apr 2026 17:42:30 +0000 (18:42 +0100)
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.

Include/internal/pycore_interp_structs.h
Modules/gcmodule.c
Python/gc.c
Python/gc_free_threading.c

index f76d4f41c55119769321b6a531645c72eab00f1b..c4b084642668a9f59d8b3401c508483b1f8b3ae5 100644 (file)
@@ -248,7 +248,7 @@ struct _gc_runtime_state {
     struct gc_generation old[2];
     /* a permanent generation which won't be collected */
     struct gc_generation permanent_generation;
-    struct gc_stats generation_stats;
+    struct gc_stats *generation_stats;
     /* true if we are currently running the collector */
     int collecting;
     // The frame that started the current collection. It might be NULL even when
index c21b61589bd261d76e3f62aa323377cc06be3838..8da28130e9da9aeacfa71721e6e0a7833f68b6d6 100644 (file)
@@ -347,9 +347,9 @@ gc_get_stats_impl(PyObject *module)
     /* To get consistent values despite allocations while constructing
        the result list, we use a snapshot of the running stats. */
     GCState *gcstate = get_gc_state();
-    stats[0] = gcstate->generation_stats.young.items[gcstate->generation_stats.young.index];
-    stats[1] = gcstate->generation_stats.old[0].items[gcstate->generation_stats.old[0].index];
-    stats[2] = gcstate->generation_stats.old[1].items[gcstate->generation_stats.old[1].index];
+    stats[0] = gcstate->generation_stats->young.items[gcstate->generation_stats->young.index];
+    stats[1] = gcstate->generation_stats->old[0].items[gcstate->generation_stats->old[0].index];
+    stats[2] = gcstate->generation_stats->old[1].items[gcstate->generation_stats->old[1].index];
 
     PyObject *result = PyList_New(0);
     if (result == NULL)
index 7bca40f6e3f58e30bfd894da443a6b3ddf4973db..284ac725d37ac60360cbdc7475248968b123dc22 100644 (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
index 7ad60a73a56a6957a4a967f4ff376457ae61952b..4b46ca04f56b201513de708fa68d2203b62f395f 100644 (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.