From 2947aa41d4e6aa340edb0979c3e59bb6b6aca3a4 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 26 Jan 2026 09:08:27 -0500 Subject: [PATCH 1/9] [ruby/prism] Use each_line to avoid allocating array Though very unlikely, it could potentially allocate a large array of whitespace. https://github.com/ruby/prism/commit/3389947819 --- lib/prism/lex_compat.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/prism/lex_compat.rb b/lib/prism/lex_compat.rb index 775be2759ad722..523ad39586b4be 100644 --- a/lib/prism/lex_compat.rb +++ b/lib/prism/lex_compat.rb @@ -659,13 +659,14 @@ def result IgnoreStateToken.new([[lineno, column], event, value, lex_state]) when :on_words_sep # Ripper emits one token each per line. - lines = value.lines - lines[0...-1].each do |whitespace| - tokens << Token.new([[lineno, column], event, whitespace, lex_state]) - lineno += 1 - column = 0 + value.each_line.with_index do |line, index| + if index > 0 + lineno += 1 + column = 0 + end + tokens << Token.new([[lineno, column], event, line, lex_state]) end - Token.new([[lineno, column], event, lines.last, lex_state]) + tokens.pop when :on_regexp_end # On regex end, Ripper scans and then sets end state, so the ripper # lexed output is begin, when it should be end. prism sets lex state From 5add7c3ea9a13e657fc7cba78b2633b9548a55aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Mon, 26 Jan 2026 16:17:35 +0100 Subject: [PATCH 2/9] Fix RUBY_MN_THREADS sleep returning prematurely (#15868) timer_thread_check_exceed() was returning true when the remaining time was less than 1ms, treating it as "too short time". This caused sub-millisecond sleeps (like sleep(0.0001)) to return immediately instead of actually sleeping. The fix removes this optimization that was incorrectly short-circuiting short sleep durations. Now the timeout is only considered exceeded when the actual deadline has passed. Note: There's still a separate performance issue where MN_THREADS mode is slower for sub-millisecond sleeps due to the timer thread using millisecond-resolution polling. This will require a separate fix to use sub-millisecond timeouts in kqueue/epoll. [Bug #21836] --- test/ruby/test_thread.rb | 12 ++++++++++++ thread_pthread.c | 10 +--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/test/ruby/test_thread.rb b/test/ruby/test_thread.rb index 7f5dbe91556eb6..b2d8e73693807c 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -1652,4 +1652,16 @@ def test_mutexes_locked_in_fiber_dont_have_aba_issue_with_new_fibers end end; end + + # [Bug #21836] + def test_mn_threads_sub_millisecond_sleep + assert_separately([{'RUBY_MN_THREADS' => '1'}], "#{<<~"begin;"}\n#{<<~'end;'}", timeout: 30) + begin; + t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 1000.times { sleep 0.0001 } + t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + elapsed = t1 - t0 + assert_operator elapsed, :>=, 0.1, "sub-millisecond sleeps should not return immediately" + end; + end end diff --git a/thread_pthread.c b/thread_pthread.c index 9c7754067bcdf9..542690eca02f92 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -2947,15 +2947,7 @@ timer_thread_check_signal(rb_vm_t *vm) static bool timer_thread_check_exceed(rb_hrtime_t abs, rb_hrtime_t now) { - if (abs < now) { - return true; - } - else if (abs - now < RB_HRTIME_PER_MSEC) { - return true; // too short time - } - else { - return false; - } + return abs <= now; } static rb_thread_t * From c21f3490d1f28b43564639ae8563bc2e02e828a4 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Wed, 14 Jan 2026 21:51:49 +0000 Subject: [PATCH 3/9] Implement a fast path for sweeping (gc_sweep_fast_path_p). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Feature #21846] There is a single path through our GC Sweeping code, and we always call rb_gc_obj_free_vm_weak_references and rb_gc_obj_free before adding the object back to the freelist. We do this even when the object has no external resources that require being free'd and has no weak references pointing to it. This commit introduces a conservative fast path through gc_sweep_plane that uses the object flags to identify certain cases where these calls can be skipped - for these objects we just add them straight back on the freelist. Any object for which gc_sweep_fast_path_p returns false will use the current full sweep code (referred to here as the slow path). Currently there are 2 checks that will _always_ require an object to go down the slow path: 1. Has it's object_id been observed and stored in the id2ref_table 2. Has it got generic ivars in the gen_fields table If neither of these are true, then we run some flag checks on the object and send the following cases down the fast path: - Objects that are not heap allocated - Embedded strings that aren't in the fstring table - Embedded Arrays - Embedded Hashes - Embedded Bignums - Embedded Strings - Floats, Rationals and Complex - Various IMEMO subtypes that do no allocation We've benchmarked this code using ruby-bench as well as the gcbench benchmarks inside Ruby (benchmarks/gc) and this patch results in a modest speed improvement on almost all of the headline benchmarks (2% in railsbench with YJIT enabled), and an observable 30% improvement in time spent sweeping during the GC benchmarks: ``` master: ruby 4.1.0dev (2026-01-19T12:03:33Z master 859920dfd2) +YJIT +PRISM [x86_64-linux] experiment: ruby 4.1.0dev (2026-01-16T21:36:46Z mvh-sweep-fast-pat.. c3ffe377a1) +YJIT +PRISM [x86_64-linux] -------------- ----------- ---------- --------------- ---------- ------------------ ----------------- bench master (ms) stddev (%) experiment (ms) stddev (%) experiment 1st itr master/experiment lobsters N/A N/A N/A N/A N/A N/A activerecord 132.5 0.9 132.5 1.0 1.056 1.001 chunky-png 577.2 0.4 580.1 0.4 0.994 0.995 erubi-rails 902.9 0.2 894.3 0.2 1.040 1.010 hexapdf 1763.9 3.3 1760.6 3.7 1.027 1.002 liquid-c 56.9 0.6 56.7 1.4 1.004 1.003 liquid-compile 46.3 2.1 46.1 2.1 1.005 1.004 liquid-render 77.8 0.8 75.1 0.9 1.023 1.036 mail 114.7 0.4 113.0 1.4 1.054 1.015 psych-load 1635.4 1.4 1625.9 0.5 0.988 1.006 railsbench 1685.4 2.4 1650.1 2.0 0.989 1.021 rubocop 133.5 8.1 130.3 7.8 1.002 1.024 ruby-lsp 140.3 1.9 137.5 1.8 1.007 1.020 sequel 64.6 0.7 63.9 0.7 1.003 1.011 shipit 1196.2 4.3 1181.5 4.2 1.003 1.012 -------------- ----------- ---------- --------------- ---------- ------------------ ----------------- Legend: - experiment 1st itr: ratio of master/experiment time for the first benchmarking iteration. - master/experiment: ratio of master/experiment time. Higher is better for experiment. Above 1 represents a speedup. ``` ``` Benchmark │ Wall(B) Sweep(B) Mark(B) │ Wall(E) Sweep(E) Mark(E) │ Wall Δ Sweep Δ ───────────────┼─────────────────────────────────┼─────────────────────────────────┼────────────────── null │ 0.000s 1ms 4ms │ 0.000s 1ms 4ms │ 0% 0% hash1 │ 4.330s 875ms 46ms │ 3.960s 531ms 44ms │ +8.6% +39.3% hash2 │ 6.356s 243ms 988ms │ 6.298s 176ms 1.03s │ +0.9% +27.6% rdoc │ 37.337s 2.42s 1.09s │ 36.678s 2.11s 1.20s │ +1.8% +13.1% binary_trees │ 3.366s 426ms 252ms │ 3.082s 275ms 239ms │ +8.4% +35.4% ring │ 5.252s 14ms 2.47s │ 5.327s 12ms 2.43s │ -1.4% +14.3% redblack │ 2.966s 28ms 41ms │ 2.940s 21ms 38ms │ +0.9% +25.0% ───────────────┼─────────────────────────────────┼─────────────────────────────────┼────────────────── Legend: (B) = Baseline, (E) = Experiment, Δ = improvement (positive = faster) Wall = total wallclock, Sweep = GC sweeping time, Mark = GC marking time Times are median of 3 runs ``` These results are also borne out when YJIT is disabled: ``` master: ruby 4.1.0dev (2026-01-19T12:03:33Z master 859920dfd2) +PRISM [x86_64-linux] experiment: ruby 4.1.0dev (2026-01-16T21:36:46Z mvh-sweep-fast-pat.. c3ffe377a1) +PRISM [x86_64-linux] -------------- ----------- ---------- --------------- ---------- ------------------ ----------------- bench master (ms) stddev (%) experiment (ms) stddev (%) experiment 1st itr master/experiment lobsters N/A N/A N/A N/A N/A N/A activerecord 389.6 0.3 377.5 0.3 1.032 1.032 chunky-png 1123.4 0.2 1109.2 0.2 1.013 1.013 erubi-rails 1754.3 0.1 1725.7 0.1 1.035 1.017 hexapdf 3346.5 0.9 3326.9 0.7 1.003 1.006 liquid-c 84.0 0.5 83.5 0.5 0.992 1.006 liquid-compile 74.0 1.5 73.5 1.4 1.011 1.008 liquid-render 199.9 0.4 199.6 0.4 1.000 1.002 mail 177.8 0.4 176.4 0.4 1.069 1.008 psych-load 2749.6 0.7 2777.0 0.0 0.980 0.990 railsbench 2983.0 1.0 2965.5 0.8 1.041 1.006 rubocop 228.8 1.0 227.5 1.2 1.015 1.005 ruby-lsp 221.8 0.9 216.1 0.8 1.011 1.026 sequel 89.1 0.5 89.1 1.8 1.005 1.000 shipit 2385.6 1.6 2371.8 1.0 1.002 1.006 -------------- ----------- ---------- --------------- ---------- ------------------ ----------------- Legend: - experiment 1st itr: ratio of master/experiment time for the first benchmarking iteration. - master/experiment: ratio of master/experiment time. Higher is better for experiment. Above 1 represents a speedup. ``` ``` Benchmark │ Wall(B) Sweep(B) Mark(B) │ Wall(E) Sweep(E) Mark(E) │ Wall Δ Sweep Δ ───────────────┼─────────────────────────────────┼─────────────────────────────────┼────────────────── null │ 0.000s 1ms 4ms │ 0.000s 1ms 3ms │ 0% 0% hash1 │ 4.349s 877ms 45ms │ 4.045s 532ms 44ms │ +7.0% +39.3% hash2 │ 6.575s 235ms 967ms │ 6.540s 181ms 1.04s │ +0.5% +23.0% rdoc │ 45.782s 2.23s 1.14s │ 44.925s 1.90s 1.01s │ +1.9% +15.0% binary_trees │ 6.433s 426ms 252ms │ 6.268s 278ms 240ms │ +2.6% +34.7% ring │ 6.584s 17ms 2.33s │ 6.738s 13ms 2.33s │ -2.3% +30.8% redblack │ 13.334s 31ms 42ms │ 13.296s 24ms 107ms │ +0.3% +22.6% ───────────────┼─────────────────────────────────┼─────────────────────────────────┼────────────────── Legend: (B) = Baseline, (E) = Experiment, Δ = improvement (positive = faster) Wall = total wallclock, Sweep = GC sweeping time, Mark = GC marking time Times are median of 3 runs ``` --- gc.c | 2 +- gc/default/default.c | 163 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 133 insertions(+), 32 deletions(-) diff --git a/gc.c b/gc.c index ab0539cd3358a3..b07bcefda2a3d5 100644 --- a/gc.c +++ b/gc.c @@ -596,6 +596,7 @@ rb_gc_guarded_ptr_val(volatile VALUE *ptr, VALUE val) #endif static const char *obj_type_name(VALUE obj); +static st_table *id2ref_tbl; #include "gc/default/default.c" #if USE_MODULAR_GC && !defined(HAVE_DLOPEN) @@ -1831,7 +1832,6 @@ rb_gc_pointer_to_heap_p(VALUE obj) #define OBJ_ID_INCREMENT (RUBY_IMMEDIATE_MASK + 1) #define LAST_OBJECT_ID() (object_id_counter * OBJ_ID_INCREMENT) static VALUE id2ref_value = 0; -static st_table *id2ref_tbl = NULL; #if SIZEOF_SIZE_T == SIZEOF_LONG_LONG static size_t object_id_counter = 1; diff --git a/gc/default/default.c b/gc/default/default.c index 013c0749946e2d..ff43e38ab9afd4 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -843,6 +843,93 @@ heap_page_in_global_empty_pages_pool(rb_objspace_t *objspace, struct heap_page * #define GET_HEAP_WB_UNPROTECTED_BITS(x) (&GET_HEAP_PAGE(x)->wb_unprotected_bits[0]) #define GET_HEAP_MARKING_BITS(x) (&GET_HEAP_PAGE(x)->marking_bits[0]) + +#ifndef BUILDING_MODULAR_GC +static inline bool +gc_sweep_fast_path_p(VALUE obj) +{ + VALUE flags = RBASIC(obj)->flags; + + if (flags & FL_FINALIZE) return false; + + switch (flags & RUBY_T_MASK) { + case T_IMEMO: + switch (imemo_type(obj)) { + case imemo_constcache: + case imemo_cref: + case imemo_ifunc: + case imemo_memo: + case imemo_svar: + case imemo_throw_data: + return true; + default: + return false; + } + + case T_DATA: + if (flags & RUBY_FL_USERPRIV0) { + uintptr_t type = (uintptr_t)RTYPEDDATA(obj)->type; + if (type & TYPED_DATA_EMBEDDED) { + RUBY_DATA_FUNC dfree = ((const rb_data_type_t *)(type & TYPED_DATA_PTR_MASK))->function.dfree; + return (uintptr_t)dfree + 1 <= 1; + } + } + return false; + + case T_OBJECT: + case T_STRING: + case T_ARRAY: + case T_HASH: + case T_BIGNUM: + case T_STRUCT: + case T_FLOAT: + case T_RATIONAL: + case T_COMPLEX: + break; + + default: + return false; + } + + shape_id_t shape_id = RBASIC_SHAPE_ID(obj); + if (id2ref_tbl && rb_shape_has_object_id(shape_id)) return false; + + switch (flags & RUBY_T_MASK) { + case T_OBJECT: + return !(flags & ROBJECT_HEAP); + + case T_STRING: + if (flags & (RSTRING_NOEMBED | RSTRING_FSTR)) return false; + return !rb_shape_has_fields(shape_id); + + case T_ARRAY: + if (!(flags & RARRAY_EMBED_FLAG)) return false; + return !rb_shape_has_fields(shape_id); + + case T_HASH: + if (flags & RHASH_ST_TABLE_FLAG) return false; + return !rb_shape_has_fields(shape_id); + + case T_BIGNUM: + if (!(flags & BIGNUM_EMBED_FLAG)) return false; + return !rb_shape_has_fields(shape_id); + + case T_STRUCT: + if (!(flags & RSTRUCT_EMBED_LEN_MASK)) return false; + if (flags & RSTRUCT_GEN_FIELDS) return !rb_shape_has_fields(shape_id); + return true; + + case T_FLOAT: + case T_RATIONAL: + case T_COMPLEX: + return !rb_shape_has_fields(shape_id); + + default: + UNREACHABLE_RETURN(false); + } +} +#endif + #define RVALUE_AGE_BITMAP_INDEX(n) (NUM_IN_PAGE(n) / (BITS_BITLENGTH / RVALUE_AGE_BIT_COUNT)) #define RVALUE_AGE_BITMAP_OFFSET(n) ((NUM_IN_PAGE(n) % (BITS_BITLENGTH / RVALUE_AGE_BIT_COUNT)) * RVALUE_AGE_BIT_COUNT) @@ -3481,15 +3568,34 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit rb_asan_unpoison_object(vp, false); if (bitset & 1) { switch (BUILTIN_TYPE(vp)) { - default: /* majority case */ - gc_report(2, objspace, "page_sweep: free %p\n", (void *)p); + case T_MOVED: + if (objspace->flags.during_compacting) { + /* The sweep cursor shouldn't have made it to any + * T_MOVED slots while the compact flag is enabled. + * The sweep cursor and compact cursor move in + * opposite directions, and when they meet references will + * get updated and "during_compacting" should get disabled */ + rb_bug("T_MOVED shouldn't be seen until compaction is finished"); + } + gc_report(3, objspace, "page_sweep: %s is added to freelist\n", rb_obj_info(vp)); + ctx->empty_slots++; + RVALUE_AGE_SET_BITMAP(vp, 0); + heap_page_add_freeobj(objspace, sweep_page, vp); + break; + case T_ZOMBIE: + /* already counted */ + break; + case T_NONE: + ctx->empty_slots++; /* already freed */ + break; + + default: #if RGENGC_CHECK_MODE if (!is_full_marking(objspace)) { if (RVALUE_OLD_P(objspace, vp)) rb_bug("page_sweep: %p - old while minor GC.", (void *)p); if (RVALUE_REMEMBERED(objspace, vp)) rb_bug("page_sweep: %p - remembered.", (void *)p); } #endif - if (RVALUE_WB_UNPROTECTED(objspace, vp)) CLEAR_IN_BITMAP(GET_HEAP_WB_UNPROTECTED_BITS(vp), vp); #if RGENGC_CHECK_MODE @@ -3501,42 +3607,37 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit #undef CHECK #endif - rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ); +#ifndef BUILDING_MODULAR_GC + if (gc_sweep_fast_path_p(vp)) { + if (UNLIKELY(objspace->hook_events & RUBY_INTERNAL_EVENT_FREEOBJ)) { + rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ); + } - rb_gc_obj_free_vm_weak_references(vp); - if (rb_gc_obj_free(objspace, vp)) { - // always add free slots back to the swept pages freelist, - // so that if we're compacting, we can re-use the slots (void)VALGRIND_MAKE_MEM_UNDEFINED((void*)p, BASE_SLOT_SIZE); RVALUE_AGE_SET_BITMAP(vp, 0); heap_page_add_freeobj(objspace, sweep_page, vp); - gc_report(3, objspace, "page_sweep: %s is added to freelist\n", rb_obj_info(vp)); + gc_report(3, objspace, "page_sweep: %s (fast path) added to freelist\n", rb_obj_info(vp)); ctx->freed_slots++; } - else { - ctx->final_slots++; - } - break; + else +#endif + { + gc_report(2, objspace, "page_sweep: free %p\n", (void *)p); - case T_MOVED: - if (objspace->flags.during_compacting) { - /* The sweep cursor shouldn't have made it to any - * T_MOVED slots while the compact flag is enabled. - * The sweep cursor and compact cursor move in - * opposite directions, and when they meet references will - * get updated and "during_compacting" should get disabled */ - rb_bug("T_MOVED shouldn't be seen until compaction is finished"); + rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ); + + rb_gc_obj_free_vm_weak_references(vp); + if (rb_gc_obj_free(objspace, vp)) { + (void)VALGRIND_MAKE_MEM_UNDEFINED((void*)p, BASE_SLOT_SIZE); + RVALUE_AGE_SET_BITMAP(vp, 0); + heap_page_add_freeobj(objspace, sweep_page, vp); + gc_report(3, objspace, "page_sweep: %s is added to freelist\n", rb_obj_info(vp)); + ctx->freed_slots++; + } + else { + ctx->final_slots++; + } } - gc_report(3, objspace, "page_sweep: %s is added to freelist\n", rb_obj_info(vp)); - ctx->empty_slots++; - RVALUE_AGE_SET_BITMAP(vp, 0); - heap_page_add_freeobj(objspace, sweep_page, vp); - break; - case T_ZOMBIE: - /* already counted */ - break; - case T_NONE: - ctx->empty_slots++; /* already freed */ break; } } From 211714f1bfd8d0927c704713545e37f18cc75229 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 20 Jan 2026 18:49:03 +0000 Subject: [PATCH 4/9] Clarify the use of some FLAGS --- gc/default/default.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gc/default/default.c b/gc/default/default.c index ff43e38ab9afd4..b4c6b9819a3d4f 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -867,10 +867,13 @@ gc_sweep_fast_path_p(VALUE obj) } case T_DATA: - if (flags & RUBY_FL_USERPRIV0) { + if (flags & RUBY_TYPED_FL_IS_TYPED_DATA) { uintptr_t type = (uintptr_t)RTYPEDDATA(obj)->type; if (type & TYPED_DATA_EMBEDDED) { RUBY_DATA_FUNC dfree = ((const rb_data_type_t *)(type & TYPED_DATA_PTR_MASK))->function.dfree; + // Fast path for embedded T_DATA with no custom free function. + // True when dfree is NULL (RUBY_NEVER_FREE) or -1 (RUBY_TYPED_DEFAULT_FREE). + // Single comparison used instead of two equality checks for performance. return (uintptr_t)dfree + 1 <= 1; } } From efde37b7122b23775b906ad90cf4e88e05b756a8 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 22 Jan 2026 22:28:24 +0000 Subject: [PATCH 5/9] Move the gc fast path out of the default GC impl It relies too much on VM level concerns, such that it can't be built with modular GC enabled. We'll move it into the VM, and then expose it to the GC implementations so they can use it. --- gc.c | 100 +++++++++++++++++++++++++++++++++++++++++++ gc/default/default.c | 92 ++------------------------------------- gc/gc.h | 1 + 3 files changed, 104 insertions(+), 89 deletions(-) diff --git a/gc.c b/gc.c index b07bcefda2a3d5..f8d19fb072289c 100644 --- a/gc.c +++ b/gc.c @@ -1243,6 +1243,106 @@ rb_gc_handle_weak_references(VALUE obj) } } +/* + * Returns true if the object requires a full rb_gc_obj_free() call during sweep, + * false if it can be freed quickly without calling destructors or cleanup. + * + * Objects that return false are: + * - Simple embedded objects without external allocations + * - Objects without finalizers + * - Objects without object IDs registered in id2ref + * - Objects without generic instance variables + * + * This is used by the GC sweep fast path to avoid function call overhead + * for the majority of simple objects. + */ +bool +rb_gc_obj_free_on_sweep_p(VALUE obj) +{ + VALUE flags = RBASIC(obj)->flags; + + if (flags & FL_FINALIZE) return true; + + switch (flags & RUBY_T_MASK) { + case T_IMEMO: + switch (imemo_type(obj)) { + case imemo_constcache: + case imemo_cref: + case imemo_ifunc: + case imemo_memo: + case imemo_svar: + case imemo_throw_data: + return false; + default: + return true; + } + + case T_DATA: + if (flags & RUBY_TYPED_FL_IS_TYPED_DATA) { + uintptr_t type = (uintptr_t)RTYPEDDATA(obj)->type; + if (type & TYPED_DATA_EMBEDDED) { + RUBY_DATA_FUNC dfree = ((const rb_data_type_t *)(type & TYPED_DATA_PTR_MASK))->function.dfree; + // Fast path for embedded T_DATA with no custom free function. + // True when dfree is NULL (RUBY_NEVER_FREE) or -1 (RUBY_TYPED_DEFAULT_FREE). + if ((uintptr_t)dfree + 1 <= 1) return false; + } + } + return true; + + case T_OBJECT: + case T_STRING: + case T_ARRAY: + case T_HASH: + case T_BIGNUM: + case T_STRUCT: + case T_FLOAT: + case T_RATIONAL: + case T_COMPLEX: + break; + + default: + return true; + } + + shape_id_t shape_id = RBASIC_SHAPE_ID(obj); + if (id2ref_tbl && rb_shape_has_object_id(shape_id)) return true; + + switch (flags & RUBY_T_MASK) { + case T_OBJECT: + if (flags & ROBJECT_HEAP) return true; + return false; + + case T_STRING: + if (flags & (RSTRING_NOEMBED | RSTRING_FSTR)) return true; + return rb_shape_has_fields(shape_id); + + case T_ARRAY: + if (!(flags & RARRAY_EMBED_FLAG)) return true; + return rb_shape_has_fields(shape_id); + + case T_HASH: + if (flags & RHASH_ST_TABLE_FLAG) return true; + return rb_shape_has_fields(shape_id); + + case T_BIGNUM: + if (!(flags & BIGNUM_EMBED_FLAG)) return true; + return rb_shape_has_fields(shape_id); + + case T_STRUCT: + if (!(flags & RSTRUCT_EMBED_LEN_MASK)) return true; + if (flags & RSTRUCT_GEN_FIELDS) return rb_shape_has_fields(shape_id); + return false; + + case T_FLOAT: + case T_RATIONAL: + case T_COMPLEX: + return rb_shape_has_fields(shape_id); + + default: + UNREACHABLE_RETURN(true); + } +} + static void io_fptr_finalize(void *fptr) { diff --git a/gc/default/default.c b/gc/default/default.c index b4c6b9819a3d4f..0f2ecdfaeb4612 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -844,94 +844,11 @@ heap_page_in_global_empty_pages_pool(rb_objspace_t *objspace, struct heap_page * #define GET_HEAP_MARKING_BITS(x) (&GET_HEAP_PAGE(x)->marking_bits[0]) -#ifndef BUILDING_MODULAR_GC static inline bool gc_sweep_fast_path_p(VALUE obj) { - VALUE flags = RBASIC(obj)->flags; - - if (flags & FL_FINALIZE) return false; - - switch (flags & RUBY_T_MASK) { - case T_IMEMO: - switch (imemo_type(obj)) { - case imemo_constcache: - case imemo_cref: - case imemo_ifunc: - case imemo_memo: - case imemo_svar: - case imemo_throw_data: - return true; - default: - return false; - } - - case T_DATA: - if (flags & RUBY_TYPED_FL_IS_TYPED_DATA) { - uintptr_t type = (uintptr_t)RTYPEDDATA(obj)->type; - if (type & TYPED_DATA_EMBEDDED) { - RUBY_DATA_FUNC dfree = ((const rb_data_type_t *)(type & TYPED_DATA_PTR_MASK))->function.dfree; - // Fast path for embedded T_DATA with no custom free function. - // True when dfree is NULL (RUBY_NEVER_FREE) or -1 (RUBY_TYPED_DEFAULT_FREE). - // Single comparison used instead of two equality checks for performance. - return (uintptr_t)dfree + 1 <= 1; - } - } - return false; - - case T_OBJECT: - case T_STRING: - case T_ARRAY: - case T_HASH: - case T_BIGNUM: - case T_STRUCT: - case T_FLOAT: - case T_RATIONAL: - case T_COMPLEX: - break; - - default: - return false; - } - - shape_id_t shape_id = RBASIC_SHAPE_ID(obj); - if (id2ref_tbl && rb_shape_has_object_id(shape_id)) return false; - - switch (flags & RUBY_T_MASK) { - case T_OBJECT: - return !(flags & ROBJECT_HEAP); - - case T_STRING: - if (flags & (RSTRING_NOEMBED | RSTRING_FSTR)) return false; - return !rb_shape_has_fields(shape_id); - - case T_ARRAY: - if (!(flags & RARRAY_EMBED_FLAG)) return false; - return !rb_shape_has_fields(shape_id); - - case T_HASH: - if (flags & RHASH_ST_TABLE_FLAG) return false; - return !rb_shape_has_fields(shape_id); - - case T_BIGNUM: - if (!(flags & BIGNUM_EMBED_FLAG)) return false; - return !rb_shape_has_fields(shape_id); - - case T_STRUCT: - if (!(flags & RSTRUCT_EMBED_LEN_MASK)) return false; - if (flags & RSTRUCT_GEN_FIELDS) return !rb_shape_has_fields(shape_id); - return true; - - case T_FLOAT: - case T_RATIONAL: - case T_COMPLEX: - return !rb_shape_has_fields(shape_id); - - default: - UNREACHABLE_RETURN(false); - } + return !rb_gc_obj_free_on_sweep_p(obj); } -#endif #define RVALUE_AGE_BITMAP_INDEX(n) (NUM_IN_PAGE(n) / (BITS_BITLENGTH / RVALUE_AGE_BIT_COUNT)) #define RVALUE_AGE_BITMAP_OFFSET(n) ((NUM_IN_PAGE(n) % (BITS_BITLENGTH / RVALUE_AGE_BIT_COUNT)) * RVALUE_AGE_BIT_COUNT) @@ -3610,9 +3527,8 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit #undef CHECK #endif -#ifndef BUILDING_MODULAR_GC if (gc_sweep_fast_path_p(vp)) { - if (UNLIKELY(objspace->hook_events & RUBY_INTERNAL_EVENT_FREEOBJ)) { + if (RB_UNLIKELY(objspace->hook_events & RUBY_INTERNAL_EVENT_FREEOBJ)) { rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ); } @@ -3622,9 +3538,7 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit gc_report(3, objspace, "page_sweep: %s (fast path) added to freelist\n", rb_obj_info(vp)); ctx->freed_slots++; } - else -#endif - { + else { gc_report(2, objspace, "page_sweep: free %p\n", (void *)p); rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ); diff --git a/gc/gc.h b/gc/gc.h index 097ddb93949a0b..196e25cf05ffaa 100644 --- a/gc/gc.h +++ b/gc/gc.h @@ -100,6 +100,7 @@ MODULAR_GC_FN void rb_gc_after_updating_jit_code(void); MODULAR_GC_FN bool rb_gc_obj_shareable_p(VALUE); MODULAR_GC_FN void rb_gc_rp(VALUE); MODULAR_GC_FN void rb_gc_handle_weak_references(VALUE obj); +MODULAR_GC_FN bool rb_gc_obj_free_on_sweep_p(VALUE obj); #if USE_MODULAR_GC MODULAR_GC_FN bool rb_gc_event_hook_required_p(rb_event_flag_t event); From 8e73aa7ffe6f5504be9f1a90e5d9dcbe3ae8376a Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Fri, 23 Jan 2026 16:11:17 +0000 Subject: [PATCH 6/9] We don't need this wrapper function anymore --- gc/default/default.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 0f2ecdfaeb4612..c057809e4eaf3f 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -844,12 +844,6 @@ heap_page_in_global_empty_pages_pool(rb_objspace_t *objspace, struct heap_page * #define GET_HEAP_MARKING_BITS(x) (&GET_HEAP_PAGE(x)->marking_bits[0]) -static inline bool -gc_sweep_fast_path_p(VALUE obj) -{ - return !rb_gc_obj_free_on_sweep_p(obj); -} - #define RVALUE_AGE_BITMAP_INDEX(n) (NUM_IN_PAGE(n) / (BITS_BITLENGTH / RVALUE_AGE_BIT_COUNT)) #define RVALUE_AGE_BITMAP_OFFSET(n) ((NUM_IN_PAGE(n) % (BITS_BITLENGTH / RVALUE_AGE_BIT_COUNT)) * RVALUE_AGE_BIT_COUNT) @@ -3527,7 +3521,7 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit #undef CHECK #endif - if (gc_sweep_fast_path_p(vp)) { + if (!rb_gc_obj_free_on_sweep_p(vp)) { if (RB_UNLIKELY(objspace->hook_events & RUBY_INTERNAL_EVENT_FREEOBJ)) { rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ); } From 7444f415db75c1436e11b61a4ce2f461158d234c Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Fri, 23 Jan 2026 16:45:57 +0000 Subject: [PATCH 7/9] rename rb_gc_obj_free_on_sweep -> rb_gc_obj_needs_cleanup_p --- gc.c | 2 +- gc/default/default.c | 2 +- gc/gc.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gc.c b/gc.c index f8d19fb072289c..d519a214178626 100644 --- a/gc.c +++ b/gc.c @@ -1257,7 +1257,7 @@ rb_gc_handle_weak_references(VALUE obj) * for the majority of simple objects. */ bool -rb_gc_obj_free_on_sweep_p(VALUE obj) +rb_gc_obj_needs_cleanup_p(VALUE obj) { VALUE flags = RBASIC(obj)->flags; diff --git a/gc/default/default.c b/gc/default/default.c index c057809e4eaf3f..5758fe188555d2 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -3521,7 +3521,7 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit #undef CHECK #endif - if (!rb_gc_obj_free_on_sweep_p(vp)) { + if (!rb_gc_obj_needs_cleanup_p(vp)) { if (RB_UNLIKELY(objspace->hook_events & RUBY_INTERNAL_EVENT_FREEOBJ)) { rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ); } diff --git a/gc/gc.h b/gc/gc.h index 196e25cf05ffaa..5979b4a00193e2 100644 --- a/gc/gc.h +++ b/gc/gc.h @@ -100,7 +100,7 @@ MODULAR_GC_FN void rb_gc_after_updating_jit_code(void); MODULAR_GC_FN bool rb_gc_obj_shareable_p(VALUE); MODULAR_GC_FN void rb_gc_rp(VALUE); MODULAR_GC_FN void rb_gc_handle_weak_references(VALUE obj); -MODULAR_GC_FN bool rb_gc_obj_free_on_sweep_p(VALUE obj); +MODULAR_GC_FN bool rb_gc_obj_needs_cleanup_p(VALUE obj); #if USE_MODULAR_GC MODULAR_GC_FN bool rb_gc_event_hook_required_p(rb_event_flag_t event); From d15117e2937b78b0868e0f41336f6350bbf1a1c4 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Mon, 26 Jan 2026 10:48:48 +0000 Subject: [PATCH 8/9] BIGNUM can't have fields other than object_id --- gc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gc.c b/gc.c index d519a214178626..d1b504ed001e17 100644 --- a/gc.c +++ b/gc.c @@ -1326,7 +1326,7 @@ rb_gc_obj_needs_cleanup_p(VALUE obj) case T_BIGNUM: if (!(flags & BIGNUM_EMBED_FLAG)) return true; - return rb_shape_has_fields(shape_id); + return false; case T_STRUCT: if (!(flags & RSTRUCT_EMBED_LEN_MASK)) return true; From 3c634893e245c578181e8337b4025d1f673d77e8 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Mon, 26 Jan 2026 12:46:00 +0000 Subject: [PATCH 9/9] Remove the unnecesary integer comparison Most compilers will optimise this anyway --- gc.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gc.c b/gc.c index d1b504ed001e17..f1c7f834d0f70c 100644 --- a/gc.c +++ b/gc.c @@ -1282,9 +1282,7 @@ rb_gc_obj_needs_cleanup_p(VALUE obj) uintptr_t type = (uintptr_t)RTYPEDDATA(obj)->type; if (type & TYPED_DATA_EMBEDDED) { RUBY_DATA_FUNC dfree = ((const rb_data_type_t *)(type & TYPED_DATA_PTR_MASK))->function.dfree; - // Fast path for embedded T_DATA with no custom free function. - // True when dfree is NULL (RUBY_NEVER_FREE) or -1 (RUBY_TYPED_DEFAULT_FREE). - if ((uintptr_t)dfree + 1 <= 1) return false; + return (dfree == RUBY_NEVER_FREE || dfree == RUBY_TYPED_DEFAULT_FREE); } } return true;