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 <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-02-12 13:38:21 +09:00
parent 5d3aab8b22
commit 31fea1709f
8 changed files with 75 additions and 37 deletions
+7 -2
View File
@@ -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 */
+1 -1
View File
@@ -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
+2 -3
View File
@@ -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 */
+1 -1
View File
@@ -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 }
};
+1 -1
View File
@@ -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);
+2 -2
View File
@@ -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 }
};
+60 -26
View File
@@ -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;
}
}
/*
+1 -1
View File
@@ -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 }
};