From 31fea1709f55a9a57367e8e11f76b7587eadad05 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Thu, 12 Feb 2026 13:38:21 +0900 Subject: [PATCH] gc.c: replace gcnext gray linked list with fixed-size gray stack remove per-object gcnext pointer from MRB_OBJECT_HEADER, saving one word (8 bytes on 64-bit) per object slot. the gray list for tri-color marking is replaced by a fixed-size stack (MRB_GRAY_STACK_SIZE=1024) in mrb_gc. when the stack overflows, a linear heap rescan recovers gray objects. object slot size: 48 -> 40 bytes (16.7% reduction on 64-bit). benchmarks show up to 12% RSS reduction on object-heavy workloads with neutral performance impact. Co-authored-by: Claude --- include/mruby/gc.h | 9 +++- include/mruby/hash.h | 2 +- include/mruby/object.h | 5 +- mrbgems/mruby-catch/src/catch.c | 2 +- src/cdump.c | 2 +- src/class.c | 4 +- src/gc.c | 86 +++++++++++++++++++++++---------- src/proc.c | 2 +- 8 files changed, 75 insertions(+), 37 deletions(-) diff --git a/include/mruby/gc.h b/include/mruby/gc.h index 6ed821175..55f997efe 100644 --- a/include/mruby/gc.h +++ b/include/mruby/gc.h @@ -28,6 +28,10 @@ MRB_API void mrb_free_context(struct mrb_state *mrb, struct mrb_context *c); #define MRB_GC_ARENA_SIZE 100 #endif +#ifndef MRB_GRAY_STACK_SIZE +#define MRB_GRAY_STACK_SIZE 1024 +#endif + typedef enum { MRB_GC_STATE_ROOT = 0, MRB_GC_STATE_MARK, @@ -38,8 +42,9 @@ typedef struct mrb_gc { struct mrb_heap_page *heaps; /* all heaps pages */ struct mrb_heap_page *free_heaps;/* heaps for allocation */ struct mrb_heap_page *sweeps; /* page where sweep starts */ - struct RBasic *gray_list; /* list of gray objects to be traversed incrementally */ - struct RBasic *atomic_gray_list; /* list of objects to be traversed atomically */ + struct RBasic *gray_stack[MRB_GRAY_STACK_SIZE]; /* stack of gray objects */ + size_t gray_stack_top; /* top index of gray stack */ + 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 */ diff --git a/include/mruby/hash.h b/include/mruby/hash.h index e591172ea..44d870555 100644 --- a/include/mruby/hash.h +++ b/include/mruby/hash.h @@ -14,7 +14,7 @@ */ MRB_BEGIN_DECL -/* offset of `iv` must be 3 words */ +/* offset of `iv` must match struct RObject */ struct RHash { MRB_OBJECT_HEADER; #ifdef MRB_64BIT diff --git a/include/mruby/object.h b/include/mruby/object.h index 57e56dc83..916874b30 100644 --- a/include/mruby/object.h +++ b/include/mruby/object.h @@ -9,7 +9,6 @@ #define MRB_OBJECT_HEADER \ struct RClass *c; \ - struct RBasic *gcnext; \ enum mrb_vtype tt:8; \ unsigned int gc_color:3; \ unsigned int frozen:1; \ @@ -39,7 +38,7 @@ struct RFiber { }; #define mrb_static_assert_object_size(st) \ - mrb_static_assert(sizeof(st) <= sizeof(void*) * 6, \ - #st " size must be within 6 words") + mrb_static_assert(sizeof(st) <= sizeof(void*) * 5, \ + #st " size must be within 5 words") #endif /* MRUBY_OBJECT_H */ diff --git a/mrbgems/mruby-catch/src/catch.c b/mrbgems/mruby-catch/src/catch.c index 747d2401b..29cacfc4d 100644 --- a/mrbgems/mruby-catch/src/catch.c +++ b/mrbgems/mruby-catch/src/catch.c @@ -49,7 +49,7 @@ static const mrb_irep catch_irep = { /* Procedure object for catch method - used to identify catch blocks in call stack */ mrb_alignas(8) static const struct RProc catch_proc = { - NULL, NULL, MRB_TT_PROC, MRB_GC_RED, MRB_OBJ_IS_FROZEN, MRB_PROC_SCOPE | MRB_PROC_STRICT, + NULL, MRB_TT_PROC, MRB_GC_RED, MRB_OBJ_IS_FROZEN, MRB_PROC_SCOPE | MRB_PROC_STRICT, { &catch_irep }, NULL, { NULL } }; diff --git a/src/cdump.c b/src/cdump.c index 55bb84038..e706b45aa 100644 --- a/src/cdump.c +++ b/src/cdump.c @@ -487,7 +487,7 @@ mrb_dump_irep_cstruct(mrb_state *mrb, const mrb_irep *irep, uint8_t flags, FILE "extern\n" "#endif", initname); - fprintf(fp, "NULL,NULL,MRB_TT_PROC,MRB_GC_RED,MRB_OBJ_IS_FROZEN,0,{&%s_irep_0},NULL,{NULL},\n}};\n", initname); + fprintf(fp, "NULL,MRB_TT_PROC,MRB_GC_RED,MRB_OBJ_IS_FROZEN,0,{&%s_irep_0},NULL,{NULL},\n}};\n", initname); fputs("static void\n", fp); fprintf(fp, "%s_init_syms(mrb_state *mrb)\n", initname); fputs("{\n", fp); diff --git a/src/class.c b/src/class.c index 722e06963..fae03fd12 100644 --- a/src/class.c +++ b/src/class.c @@ -4206,7 +4206,7 @@ static const mrb_irep new_irep = { mrb_alignas(8) static const struct RProc new_proc = { - NULL, NULL, MRB_TT_PROC, MRB_GC_RED, MRB_OBJ_IS_FROZEN, MRB_PROC_SCOPE | MRB_PROC_STRICT, + NULL, MRB_TT_PROC, MRB_GC_RED, MRB_OBJ_IS_FROZEN, MRB_PROC_SCOPE | MRB_PROC_STRICT, { &new_irep }, NULL, { NULL } }; @@ -4238,7 +4238,7 @@ static const mrb_irep neq_irep = { mrb_alignas(8) static const struct RProc neq_proc = { - NULL, NULL, MRB_TT_PROC, MRB_GC_RED, MRB_OBJ_IS_FROZEN, MRB_PROC_SCOPE | MRB_PROC_STRICT, + NULL, MRB_TT_PROC, MRB_GC_RED, MRB_OBJ_IS_FROZEN, MRB_PROC_SCOPE | MRB_PROC_STRICT, { &neq_irep }, NULL, { NULL } }; diff --git a/src/gc.c b/src/gc.c index adc4c7ffb..bb5c00287 100644 --- a/src/gc.c +++ b/src/gc.c @@ -121,7 +121,7 @@ struct free_obj { struct RVALUE_initializer { MRB_OBJECT_HEADER; - char padding[sizeof(void*) * 4 - sizeof(uint32_t)]; + char padding[sizeof(void*) * 3]; }; struct RVALUE { @@ -531,8 +531,12 @@ add_gray_list(mrb_gc *gc, struct RBasic *obj) } #endif paint_gray(obj); - obj->gcnext = gc->gray_list; - gc->gray_list = obj; + if (gc->gray_stack_top < MRB_GRAY_STACK_SIZE) { + gc->gray_stack[gc->gray_stack_top++] = obj; + } + else { + gc->gray_overflow = TRUE; + } } static void @@ -914,8 +918,8 @@ root_scan_phase(mrb_state *mrb, mrb_gc *gc) int i, e; if (!is_minor_gc(gc)) { - gc->gray_list = NULL; - gc->atomic_gray_list = NULL; + gc->gray_stack_top = 0; + gc->gray_overflow = FALSE; } mrb_gc_mark_gv(mrb); @@ -964,13 +968,37 @@ root_scan_phase(mrb_state *mrb, mrb_gc *gc) #endif } +static void +gc_gray_rescan(mrb_state *mrb, mrb_gc *gc) +{ + mrb_heap_page *page = gc->heaps; + + gc->gray_overflow = FALSE; + while (page) { + RVALUE *p = page->objects; + RVALUE *e = p + MRB_HEAP_PAGE_SIZE; + for (; p < e; p++) { + if (is_gray(&p->as.basic) && p->as.basic.tt != MRB_TT_FREE) { + if (gc->gray_stack_top >= MRB_GRAY_STACK_SIZE) { + gc->gray_overflow = TRUE; + return; + } + gc->gray_stack[gc->gray_stack_top++] = &p->as.basic; + } + } + page = page->next; + } +} + static void gc_mark_gray_list(mrb_state *mrb, mrb_gc *gc) { - while (gc->gray_list) { - struct RBasic *obj = gc->gray_list; - gc->gray_list = obj->gcnext; - obj->gcnext = NULL; - gc_mark_children(mrb, gc, obj); + for (;;) { + while (gc->gray_stack_top > 0) { + struct RBasic *obj = gc->gray_stack[--gc->gray_stack_top]; + gc_mark_children(mrb, gc, obj); + } + if (!gc->gray_overflow) break; + gc_gray_rescan(mrb, gc); } } @@ -979,11 +1007,18 @@ incremental_marking_phase(mrb_state *mrb, mrb_gc *gc, size_t limit) { size_t tried_marks = 0; - while (gc->gray_list && tried_marks < limit) { - struct RBasic *obj = gc->gray_list; - gc->gray_list = obj->gcnext; - obj->gcnext = NULL; - tried_marks += gc_mark_children(mrb, gc, obj); + while (tried_marks < limit) { + if (gc->gray_stack_top > 0) { + struct RBasic *obj = gc->gray_stack[--gc->gray_stack_top]; + tried_marks += gc_mark_children(mrb, gc, obj); + } + else if (gc->gray_overflow) { + gc_gray_rescan(mrb, gc); + if (gc->gray_stack_top == 0) break; + } + else { + break; + } } return tried_marks; @@ -1027,18 +1062,12 @@ final_marking_phase(mrb_state *mrb, mrb_gc *gc) #endif gc_mark_gray_list(mrb, gc); - mrb_assert(gc->gray_list == NULL); - gc->gray_list = gc->atomic_gray_list; - gc->atomic_gray_list = NULL; - gc_mark_gray_list(mrb, gc); - mrb_assert(gc->gray_list == NULL); } static void prepare_incremental_sweep(mrb_state *mrb, mrb_gc *gc) { - // mrb_assert(gc->atomic_gray_list == NULL); - // mrb_assert(gc->gray_list == NULL); + // mrb_assert(gc->gray_stack_top == 0); gc->state = MRB_GC_STATE_SWEEP; gc->sweeps = NULL; gc->live_after_mark = gc->live; @@ -1131,7 +1160,7 @@ incremental_gc(mrb_state *mrb, mrb_gc *gc, size_t limit) flip_white_part(gc); return 0; case MRB_GC_STATE_MARK: - if (gc->gray_list) { + if (gc->gray_stack_top > 0 || gc->gray_overflow) { return incremental_marking_phase(mrb, gc, limit); } else { @@ -1190,7 +1219,8 @@ clear_all_old(mrb_state *mrb, mrb_gc *gc) incremental_gc_finish(mrb, gc); gc->generational = TRUE; /* The gray objects have already been painted as white */ - gc->atomic_gray_list = gc->gray_list = NULL; + gc->gray_stack_top = 0; + gc->gray_overflow = FALSE; } MRB_API void @@ -1318,8 +1348,12 @@ mrb_write_barrier(mrb_state *mrb, struct RBasic *obj) mrb_assert(!is_dead(gc, obj)); mrb_assert(is_generational(gc) || gc->state != MRB_GC_STATE_ROOT); paint_gray(obj); - obj->gcnext = gc->atomic_gray_list; - gc->atomic_gray_list = obj; + if (gc->gray_stack_top < MRB_GRAY_STACK_SIZE) { + gc->gray_stack[gc->gray_stack_top++] = obj; + } + else { + gc->gray_overflow = TRUE; + } } /* diff --git a/src/proc.c b/src/proc.c index 5395aeefc..a9c4d8f0d 100644 --- a/src/proc.c +++ b/src/proc.c @@ -38,7 +38,7 @@ static const mrb_irep call_irep = { mrb_alignas(8) static const struct RProc call_proc = { - NULL, NULL, MRB_TT_PROC, MRB_GC_RED, MRB_OBJ_IS_FROZEN, MRB_PROC_SCOPE | MRB_PROC_STRICT, + NULL, MRB_TT_PROC, MRB_GC_RED, MRB_OBJ_IS_FROZEN, MRB_PROC_SCOPE | MRB_PROC_STRICT, { &call_irep }, NULL, { NULL } };