gc.c: replace threshold model with debt-based GC trigger

Replace the threshold-based GC trigger (gc->threshold vs gc->live)
with a debt model (gc->gc_debt). Each allocation increments debt;
each GC step decrements by GC_STEP_SIZE. When a cycle completes,
credit is proportional to live_after_mark * interval_ratio, giving
a natural feedback loop that adapts to allocation rate.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-03-14 22:17:32 +09:00
parent 4b866a84da
commit f0b3fdfb14
2 changed files with 18 additions and 9 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ typedef struct mrb_gc {
mrb_bool gray_overflow:1; /* gray stack overflowed; needs heap rescan */
size_t live; /* count of live objects */
size_t live_after_mark; /* old generation objects */
size_t threshold; /* threshold to start GC */
mrb_int gc_debt; /* <0:credit, >0:needs GC */
size_t oldgen_threshold; /* threshold to kick major GC */
mrb_gc_state state; /* current state of gc */
int interval_ratio;
+17 -8
View File
@@ -598,7 +598,8 @@ mrb_obj_alloc(mrb_state *mrb, enum mrb_vtype ttype, struct RClass *cls)
#ifdef MRB_GC_STRESS
mrb_full_gc(mrb);
#endif
if (gc->threshold < gc->live) {
gc->gc_debt++;
if (gc->gc_debt > 0) {
mrb_incremental_gc(mrb);
}
gc_arena_keep(mrb, gc);
@@ -1331,7 +1332,7 @@ incremental_gc_step(mrb_state *mrb, mrb_gc *gc)
break;
}
gc->threshold = gc->live + GC_STEP_SIZE;
gc->gc_debt -= (mrb_int)GC_STEP_SIZE;
}
static void
@@ -1380,9 +1381,11 @@ mrb_incremental_gc(mrb_state *mrb)
if (gc->state == MRB_GC_STATE_ROOT) {
gc->malloc_increase = 0;
mrb_assert(gc->live >= gc->live_after_mark);
gc->threshold = (gc->live_after_mark/100) * gc->interval_ratio;
if (gc->threshold < GC_STEP_SIZE) {
gc->threshold = GC_STEP_SIZE;
{
mrb_int credit = (mrb_int)((gc->live_after_mark/100) * gc->interval_ratio)
- (mrb_int)gc->live_after_mark;
if (credit < (mrb_int)GC_STEP_SIZE) credit = (mrb_int)GC_STEP_SIZE;
gc->gc_debt = -credit;
}
if (is_major_gc(gc)) {
@@ -1429,7 +1432,12 @@ mrb_full_gc(mrb_state *mrb)
}
incremental_gc_finish(mrb, gc);
gc->threshold = (gc->live_after_mark/100) * gc->interval_ratio;
{
mrb_int credit = (mrb_int)((gc->live_after_mark/100) * gc->interval_ratio)
- (mrb_int)gc->live_after_mark;
if (credit < (mrb_int)GC_STEP_SIZE) credit = (mrb_int)GC_STEP_SIZE;
gc->gc_debt = -credit;
}
if (is_generational(gc)) {
gc->oldgen_threshold = gc->live_after_mark/100 * MAJOR_GC_INC_RATIO;
@@ -1777,7 +1785,8 @@ mrb_objspace_page_slot_size(void)
* GC.stat -> Hash
*
* Returns a Hash with GC statistics.
* Keys: :live, :threshold, :state, :generational, :full
* Keys: :live, :debt, :state, :generational, :full,
* :step_limit, :malloc_increase, :malloc_threshold
* With MRB_GC_STATS: :total, :minor, :major
*
*/
@@ -1789,7 +1798,7 @@ gc_stat(mrb_state *mrb, mrb_value self)
mrb_value hash = mrb_hash_new_capa(mrb, 8);
mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "live")), mrb_int_value(mrb, (mrb_int)gc->live));
mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "threshold")), mrb_int_value(mrb, (mrb_int)gc->threshold));
mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "debt")), mrb_int_value(mrb, gc->gc_debt));
mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "state")), mrb_int_value(mrb, (mrb_int)gc->state));
mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "generational")), mrb_bool_value(gc->generational));
mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "full")), mrb_bool_value(gc->full));