From 4d03f40204cfd1f2bda5de56309deca1b8822a8d Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Sat, 14 Mar 2026 22:40:15 +0900 Subject: [PATCH] doc/internal/gc.md: update for debt model and new tuning parameters Document the debt-based GC trigger model, malloc threshold, step limit, GC.stat, and tuning guide. Co-authored-by: Claude --- doc/internal/gc.md | 102 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 13 deletions(-) diff --git a/doc/internal/gc.md b/doc/internal/gc.md index 3171bcbe1..8f84f3e17 100644 --- a/doc/internal/gc.md +++ b/doc/internal/gc.md @@ -273,7 +273,7 @@ From Ruby: `GC.generational_mode = true/false`. `mrb_obj_alloc()` is the core allocation function: 1. If `MRB_GC_STRESS` is defined, run a full GC -2. If `gc->live >= gc->threshold`, run `mrb_incremental_gc()` +2. Increment `gc->gc_debt`; if positive, run `mrb_incremental_gc()` 3. Ensure arena has space (`gc_arena_keep`) 4. Pop an object from the freelist of `gc->free_heaps` 5. If no free pages, allocate a new page (`add_heap`) @@ -299,18 +299,43 @@ The object's type is set to `MRB_TT_FREE` after freeing. ## Triggering GC -### Automatic +### Debt Model -GC runs automatically when `gc->live >= gc->threshold` during -object allocation. After each cycle: +GC uses a **debt-based feedback model** to balance allocation +rate against collection work. The key field is `gc->gc_debt` +(signed integer): + +- **Negative** = credit (GC is ahead, no collection needed) +- **Zero** = balanced +- **Positive** = debt (allocation outpacing collection, GC runs) + +Each object allocation increments `gc_debt` by 1. When debt +goes positive, `mrb_incremental_gc()` runs. Each incremental +step decrements debt by `GC_STEP_SIZE` (1024), giving credit +for many future allocations. + +When a GC cycle completes, credit is calculated from +`interval_ratio`: ```text -threshold = (live_after_mark / 100) * interval_ratio +credit = (live_after_mark / 100) * interval_ratio - live_after_mark minimum: GC_STEP_SIZE (1024) +gc_debt = -credit ``` -With default `interval_ratio = 200`, GC triggers when live objects -roughly double. +With default `interval_ratio = 200` and 1000 live objects: +`credit = (1000/100)*200 - 1000 = 1000`, so approximately 1000 +allocations can occur before the next GC cycle begins. + +### Malloc Pressure + +When `gc->malloc_threshold` is set (non-zero), the GC also +tracks bytes allocated through `mrb_realloc_simple()` in +`gc->malloc_increase`. When `malloc_increase` exceeds +`malloc_threshold`, the counter resets and an incremental GC +step runs. This captures memory pressure from large buffers +(e.g., long strings) that would otherwise be invisible to the +object-count-based debt model. ### Manual @@ -333,6 +358,7 @@ From Ruby: `GC.start`. | `MRB_GC_FIXED_ARENA` | off | Use fixed-size arena | | `MRB_GC_TURN_OFF_GENERATIONAL` | off | Disable generational mode | | `MRB_GC_STRESS` | off | Full GC on every allocation (debug) | +| `MRB_GC_STATS` | off | Enable GC statistics counters | | `MRB_USE_MALLOC_TRIM` | off | Call `malloc_trim()` after full GC | ### Runtime @@ -340,18 +366,68 @@ From Ruby: `GC.start`. From Ruby code: ```ruby -GC.interval_ratio = 200 # threshold = live * ratio / 100 -GC.step_ratio = 200 # objects per incremental step +GC.interval_ratio = 200 # controls debt credit after GC cycle +GC.step_ratio = 200 # objects per incremental step +GC.step_limit = 0 # 0=unlimited, >0=absolute step cap +GC.malloc_threshold = 0 # 0=disabled, >0=bytes to trigger GC GC.generational_mode = true -GC.start # force full GC -GC.enable # re-enable GC -GC.disable # disable GC +GC.start # force full GC +GC.enable # re-enable GC +GC.disable # disable GC ``` +### GC Statistics + +`GC.stat` returns a Hash with GC state and statistics: + +```ruby +GC.stat +# => { +# :live => 5432, # live object count +# :threshold => 1024, # GC credit (positive=headroom) +# :state => 0, # 0=root, 1=marking, 2=sweeping +# :generational => true, # generational mode enabled +# :full => false, # major GC in progress +# :step_limit => 0, # current step limit setting +# :malloc_increase => 8192, # malloc bytes since last cycle +# :malloc_threshold => 0, # current malloc threshold setting +# } +``` + +With `MRB_GC_STATS` enabled, additional keys are available: + +```ruby +# :total => 15, # total GC invocations +# :minor => 12, # minor GC count +# :major => 3, # major GC count +``` + +### Tuning Guide + +**`interval_ratio`** (default 200): Controls how many allocations +occur between GC cycles. Higher values reduce GC frequency but +increase peak memory. The debt credit after each cycle is +`(live_after_mark / 100) * interval_ratio - live_after_mark`. + +**`step_ratio`** (default 200): Controls how much work each +incremental step performs. Higher values make each step larger, +reducing total GC overhead but increasing individual pause times. + +**`step_limit`** (default 0, unlimited): Caps the maximum work +per incremental step regardless of `step_ratio`. Useful for +real-time applications that need bounded pause times. The +effective step size is `min(step_ratio calculation, step_limit)`. + +**`malloc_threshold`** (default 0, disabled): Triggers GC when +cumulative `malloc`/`realloc` bytes exceed this threshold. Useful +when applications allocate large buffers (strings, data objects) +that create memory pressure without proportional object count +increase. + ## Source Files | File | Contents | | -------------------- | --------------------------------- | -| `src/gc.c` | GC implementation (~1400 lines) | +| `src/gc.c` | GC implementation | | `include/mruby/gc.h` | `mrb_gc` structure, public GC API | | `include/mruby.h` | Arena save/restore macros |