From a292d5ccb575af7ae2daa9dd10829fb4578f5341 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Thu, 22 Dec 2022 00:36:27 +0000 Subject: [PATCH] Add dynamic thresholds to the GC --- Include/internal/pycore_gc.h | 3 ++ Include/internal/pycore_runtime_init.h | 6 ++-- Modules/gcmodule.c | 41 ++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/Include/internal/pycore_gc.h b/Include/internal/pycore_gc.h index b3abe2030a03dac..9dcb8400a0bf5eb 100644 --- a/Include/internal/pycore_gc.h +++ b/Include/internal/pycore_gc.h @@ -142,6 +142,9 @@ struct gc_generation { int threshold; /* collection threshold */ int count; /* count of allocations or collections of younger generations */ + float target; /* target number of collected objects */ + int min_threshold; /* minimum threshold */ + int max_threshold; /* maximum threshold */ }; /* Running stats per generation */ diff --git a/Include/internal/pycore_runtime_init.h b/Include/internal/pycore_runtime_init.h index cb3fce3732c79b6..7eb896aa2943a99 100644 --- a/Include/internal/pycore_runtime_init.h +++ b/Include/internal/pycore_runtime_init.h @@ -121,9 +121,9 @@ extern "C" { .enabled = 1, \ .generations = { \ /* .head is set in _PyGC_InitState(). */ \ - { .threshold = 700, }, \ - { .threshold = 10, }, \ - { .threshold = 10, }, \ + { .threshold = 700, .target = 0.2, .min_threshold=700, .max_threshold=5000 }, \ + { .threshold = 10, .target = 0.4, .min_threshold=10, .max_threshold=10000 }, \ + { .threshold = 10, .target = 0.6, .min_threshold=10, .max_threshold=50000 }, \ }, \ }, \ .static_objects = { \ diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c index 6630faa6f4471d7..e89af1c4d861e7f 100644 --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -1175,6 +1175,43 @@ handle_resurrected_objects(PyGC_Head *unreachable, PyGC_Head* still_unreachable, gc_list_merge(resurrected, old_generation); } +static void +gc_adapt_threshold(GCState *gcstate, int generation, Py_ssize_t generation_size, Py_ssize_t collected) { + Py_ssize_t threshold = gcstate->generations[generation].threshold; + float target = gcstate->generations[generation].target; + + Py_ssize_t new_threshold; + + // Calculate the current ratio of collected objects to generation size + double ratio = (double)collected / (double)generation_size; + + // Use a formula to adjust the threshold based on the difference between + // the current ratio and the target, and smoothly saturate towards the + // maximum value of MIN/MAX_THRESHOLD using a sigmoid function that grows + // saturates around 0 and 1; + + // If the ratio is greater than the target, reduce the threshold + if (ratio > target) { + Py_ssize_t min_threshold = gcstate->generations[generation].min_threshold; + double exponent = -((ratio - target)*10.0-5); + double exxp = 1.0 / (1.0 + exp(exponent)); + new_threshold = threshold + (min_threshold - threshold) * exxp; + } + // If the ratio is smaller than the target, increase the threshold + else if (ratio < target) { + Py_ssize_t max_threshold = gcstate->generations[generation].max_threshold; + double exponent = -((target - ratio)*10.0-5); + double exxp = 1.0 / (1.0 + exp(exponent)); + new_threshold = threshold + (max_threshold - threshold) * exxp; + } + // If the ratio is equal to the target, do not change the threshold + else { + new_threshold = threshold; + } + + gcstate->generations[generation].threshold = new_threshold; +} + /* This is the main function. Read this to understand how the * collection process works. */ static Py_ssize_t @@ -1226,6 +1263,8 @@ gc_collect_main(PyThreadState *tstate, int generation, old = young; validate_list(old, collecting_clear_unreachable_clear); + Py_ssize_t gen_size = gc_list_size(young); + deduce_unreachable(young, &unreachable); untrack_tuples(young); @@ -1325,6 +1364,8 @@ gc_collect_main(PyThreadState *tstate, int generation, } } + gc_adapt_threshold(gcstate, generation, gen_size, m); + /* Update stats */ if (n_collected) { *n_collected = m;