From 5cf6c842e573a55dbc89619e939fd7fa8a203b87 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 8 Apr 2026 13:10:50 +0200 Subject: [PATCH 01/34] rb_gc_obj_needs_cleanup_p: skip sweep for most imemo/fields There is only two rare cases where they need to be sweeped: - If they are so large that they're heap allocated. - If they have an object_id (and id2ref_tbl exist but we can't check that here) --- gc.c | 30 +++++++++++++++++++++++++++++- internal/imemo.h | 26 -------------------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/gc.c b/gc.c index f05b1a691375ca..3cf3bdebce2626 100644 --- a/gc.c +++ b/gc.c @@ -1291,6 +1291,34 @@ rb_gc_handle_weak_references(VALUE obj) } } +static inline bool +rb_gc_imemo_needs_cleanup_p(VALUE obj) +{ + switch (imemo_type(obj)) { + case imemo_constcache: + case imemo_cref: + case imemo_ifunc: + case imemo_memo: + case imemo_svar: + case imemo_callcache: + case imemo_throw_data: + return false; + + case imemo_env: + case imemo_ment: + case imemo_iseq: + case imemo_callinfo: + return true; + + case imemo_tmpbuf: + return ((rb_imemo_tmpbuf_t *)obj)->ptr != NULL; + + case imemo_fields: + return FL_TEST_RAW(obj, OBJ_FIELD_HEAP) || (id2ref_tbl && rb_shape_obj_has_id(obj)); + } + UNREACHABLE_RETURN(true); +} + /* * 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. @@ -1313,7 +1341,7 @@ rb_gc_obj_needs_cleanup_p(VALUE obj) switch (flags & RUBY_T_MASK) { case T_IMEMO: - return rb_imemo_needs_cleanup_p(obj); + return rb_gc_imemo_needs_cleanup_p(obj); case T_DATA: case T_OBJECT: diff --git a/internal/imemo.h b/internal/imemo.h index 4f2c4ebfbf6e98..e48832b4e5aaa0 100644 --- a/internal/imemo.h +++ b/internal/imemo.h @@ -292,30 +292,4 @@ rb_imemo_fields_complex_tbl(VALUE fields_obj) return IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table; } -static inline bool -rb_imemo_needs_cleanup_p(VALUE obj) -{ - switch (imemo_type(obj)) { - case imemo_constcache: - case imemo_cref: - case imemo_ifunc: - case imemo_memo: - case imemo_svar: - case imemo_callcache: - case imemo_throw_data: - return false; - - case imemo_env: - case imemo_ment: - case imemo_iseq: - case imemo_callinfo: - case imemo_fields: - return true; - - case imemo_tmpbuf: - return ((rb_imemo_tmpbuf_t *)obj)->ptr != NULL; - } - UNREACHABLE_RETURN(true); -} - #endif /* INTERNAL_IMEMO_H */ From d6528d631959c88db6062a39e201a0f3e89e691f Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 8 Apr 2026 20:21:10 +0200 Subject: [PATCH 02/34] compile.c: avoid needless rehash These were introduced in 165e10b6cf73f723c2f6af676b70aeb2d8cf85c9 when these hashes were allocated with `rb_hash_new()`. But since 4fb1438b9d7f617c7a8dc37935a960a24219e697 they are allocated with the right size, making the rehashing pure waste. --- compile.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/compile.c b/compile.c index 5decd5e59e742d..d39438c8ba8a76 100644 --- a/compile.c +++ b/compile.c @@ -2731,7 +2731,6 @@ iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) data.len = len; rb_hash_foreach(map, cdhash_set_label_i, (VALUE)&data); - rb_hash_rehash(map); freeze_hide_obj(map); rb_ractor_make_shareable(map); generated_iseq[code_index + 1 + j] = map; @@ -14346,7 +14345,6 @@ ibf_load_object_hash(const struct ibf_load *load, const struct ibf_object_header VALUE val = ibf_load_object(load, val_index); rb_hash_aset(obj, key, val); } - rb_hash_rehash(obj); if (header->internal) rb_obj_hide(obj); if (header->frozen) { From 82470d8b9b4a5958b5358e3526316e2b9bd9adce Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 3 Apr 2026 11:13:26 +0100 Subject: [PATCH 03/34] Allow fixed size hashes to be allocated in smaller slots When we allocate a RHash using `rb_hash_new_capa()`, if `capa` is larger than `8` it's directly allocated as an `st_stable` in a `80B` slot. However if the requested size if lesser or equal to 8, we allocate it as an `ar_table` in a `160B` slot. Since most hashes are allocated as mutable, we have to be able to accomodate as much as 8 AR_TABLE entries regardless. However there are case where we know the Hash won't ever be resized, that notably the case of all the "literal" hashes allocated by the compiler. These are immediately frozen and hidden upon being constructed, hence we can know for sure they won't ever be resized. This allows us to allocate the smaller ones in smaller slots. ``` size: 0, slot_size: 32 size: 1, slot_size: 48 size: 2, slot_size: 64 size: 3, slot_size: 80 size: 4, slot_size: 96 size: 5, slot_size: 112 size: 6, slot_size: 128 size: 7, slot_size: 144 size: 8, slot_size: 160 ``` ```ruby require "objspace" p ObjectSpace.memsize_of({}.freeze) # => 40 p ObjectSpace.memsize_of({a: 1}.freeze) # => 80 p ObjectSpace.memsize_of({a: 1, b: 2}.freeze) # => 80 p ObjectSpace.memsize_of({a: 1, b: 2, c: 3}.freeze) # => 80 p ObjectSpace.memsize_of({a: 1, b: 2, c: 3, d: 4}.freeze) # => 160 p ObjectSpace.memsize_of({a: 1, b: 2, c: 3, d: 4, e: 5, }.freeze) # => 160 p ObjectSpace.memsize_of({a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}.freeze) # => 160 p ObjectSpace.memsize_of({a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7}.freeze) # => 160 p ObjectSpace.memsize_of({a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8}.freeze) # => 160 ``` --- compile.c | 10 +++++----- hash.c | 26 +++++++++++++++++++++++--- internal/hash.h | 1 + prism_compile.c | 8 +++----- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/compile.c b/compile.c index d39438c8ba8a76..e6748d38b309da 100644 --- a/compile.c +++ b/compile.c @@ -5372,10 +5372,10 @@ compile_hash(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, int meth if (!RB_SPECIAL_CONST_P(elem[1])) RB_OBJ_SET_FROZEN_SHAREABLE(elem[1]); rb_ary_cat(ary, elem, 2); } - VALUE hash = rb_hash_new_with_size(RARRAY_LEN(ary) / 2); + VALUE hash = rb_hash_alloc_fixed_size(Qfalse, RARRAY_LEN(ary) / 2); rb_hash_bulk_insert(RARRAY_LEN(ary), RARRAY_CONST_PTR(ary), hash); RB_GC_GUARD(ary); - hash = RB_OBJ_SET_FROZEN_SHAREABLE(rb_obj_hide(hash)); + hash = RB_OBJ_SET_FROZEN_SHAREABLE(hash); /* Emit optimized code */ FLUSH_CHUNK(); @@ -12167,7 +12167,7 @@ iseq_build_from_ary_body(rb_iseq_t *iseq, LINK_ANCHOR *const anchor, case TS_CDHASH: { int i; - VALUE map = rb_hash_new_with_size(RARRAY_LEN(op)/2); + VALUE map = rb_hash_alloc_fixed_size(Qfalse, RARRAY_LEN(op)/2); RHASH_TBL_RAW(map)->type = &cdhash_type; op = rb_to_array_type(op); @@ -12179,7 +12179,7 @@ iseq_build_from_ary_body(rb_iseq_t *iseq, LINK_ANCHOR *const anchor, rb_hash_aset(map, key, (VALUE)label | 1); } RB_GC_GUARD(op); - RB_OBJ_SET_SHAREABLE(rb_obj_hide(map)); // allow mutation while compiling + RB_OBJ_SET_SHAREABLE(map); // allow mutation while compiling argv[j] = map; RB_OBJ_WRITTEN(iseq, Qundef, map); } @@ -14334,7 +14334,7 @@ static VALUE ibf_load_object_hash(const struct ibf_load *load, const struct ibf_object_header *header, ibf_offset_t offset) { long len = (long)ibf_load_small_value(load, &offset); - VALUE obj = rb_hash_new_with_size(len); + VALUE obj = header->frozen ? rb_hash_alloc_fixed_size(rb_cHash, len) : rb_hash_new_with_size(len); int i; for (i = 0; i < len; i++) { diff --git a/hash.c b/hash.c index 79dbd5d8e90f0a..700c429d2aeb49 100644 --- a/hash.c +++ b/hash.c @@ -1142,12 +1142,15 @@ ar_values(VALUE hash, st_data_t *values, st_index_t size) static ar_table* ar_copy(VALUE hash1, VALUE hash2) { + RUBY_ASSERT(rb_gc_obj_slot_size(hash1) >= sizeof(struct RHash) + sizeof(ar_table)); ar_table *old_tab = RHASH_AR_TABLE(hash2); ar_table *new_tab = RHASH_AR_TABLE(hash1); - *new_tab = *old_tab; + unsigned int bound = RHASH_AR_TABLE_BOUND(hash2); + new_tab->ar_hint.word = old_tab->ar_hint.word; + MEMCPY(&new_tab->pairs, &old_tab->pairs, ar_table_pair, bound); RHASH_AR_TABLE(hash1)->ar_hint.word = RHASH_AR_TABLE(hash2)->ar_hint.word; - RHASH_AR_TABLE_BOUND_SET(hash1, RHASH_AR_TABLE_BOUND(hash2)); + RHASH_AR_TABLE_BOUND_SET(hash1, bound); RHASH_AR_TABLE_SIZE_SET(hash1, RHASH_AR_TABLE_SIZE(hash2)); rb_gc_writebarrier_remember(hash1); @@ -1490,6 +1493,23 @@ rb_hash_new_capa(long capa) return rb_hash_new_with_size((st_index_t)capa); } +VALUE +rb_hash_alloc_fixed_size(VALUE klass, st_index_t size) +{ + VALUE ret; + if (size > RHASH_AR_TABLE_MAX_SIZE) { + ret = hash_alloc_flags(klass, 0, Qnil, true); + hash_st_table_init(ret, &objhash, size); + } + else { + size_t slot_size = sizeof(struct RHash) + offsetof(ar_table, pairs) + size * sizeof(ar_table_pair); + ret = rb_wb_protected_newobj_of(GET_EC(), klass, T_HASH, 0, slot_size); + } + + RHASH_SET_IFNONE(ret, Qnil); + return ret; +} + static VALUE hash_copy(VALUE ret, VALUE hash) { @@ -7475,7 +7495,7 @@ Init_Hash(void) rb_define_singleton_method(rb_cHash, "ruby2_keywords_hash?", rb_hash_s_ruby2_keywords_hash_p, 1); rb_define_singleton_method(rb_cHash, "ruby2_keywords_hash", rb_hash_s_ruby2_keywords_hash, 1); - rb_cHash_empty_frozen = rb_hash_freeze(rb_hash_new()); + rb_cHash_empty_frozen = rb_hash_freeze(rb_hash_alloc_fixed_size(rb_cHash, 0)); RB_OBJ_SET_SHAREABLE(rb_cHash_empty_frozen); rb_vm_register_global_object(rb_cHash_empty_frozen); diff --git a/internal/hash.h b/internal/hash.h index 03cd830506a6d6..9688478d1ee661 100644 --- a/internal/hash.h +++ b/internal/hash.h @@ -88,6 +88,7 @@ int rb_hash_stlike_delete(VALUE hash, st_data_t *pkey, st_data_t *pval); int rb_hash_stlike_foreach_with_replace(VALUE hash, st_foreach_check_callback_func *func, st_update_callback_func *replace, st_data_t arg); int rb_hash_stlike_update(VALUE hash, st_data_t key, st_update_callback_func *func, st_data_t arg); bool rb_hash_default_unredefined(VALUE hash); +VALUE rb_hash_alloc_fixed_size(VALUE klass, st_index_t size); VALUE rb_ident_hash_new_with_size(st_index_t size); void rb_hash_free(VALUE hash); RUBY_EXTERN VALUE rb_cHash_empty_frozen; diff --git a/prism_compile.c b/prism_compile.c index b693f2e05a806a..8f3f027f1898eb 100644 --- a/prism_compile.c +++ b/prism_compile.c @@ -876,11 +876,10 @@ pm_static_literal_value(rb_iseq_t *iseq, const pm_node_t *node, pm_scope_node_t rb_ary_cat(array, pair, 2); } - VALUE value = rb_hash_new_with_size(elements->size); + VALUE value = rb_hash_alloc_fixed_size(Qfalse, elements->size); rb_hash_bulk_insert(RARRAY_LEN(array), RARRAY_CONST_PTR(array), value); RB_GC_GUARD(array); - value = rb_obj_hide(value); RB_OBJ_SET_FROZEN_SHAREABLE(value); return value; } @@ -1560,10 +1559,9 @@ pm_compile_hash_elements(rb_iseq_t *iseq, const pm_node_t *node, const pm_node_l } index --; - VALUE hash = rb_hash_new_with_size(RARRAY_LEN(ary) / 2); + VALUE hash = rb_hash_alloc_fixed_size(Qfalse, RARRAY_LEN(ary) / 2); rb_hash_bulk_insert(RARRAY_LEN(ary), RARRAY_CONST_PTR(ary), hash); RB_GC_GUARD(ary); - hash = rb_obj_hide(hash); RB_OBJ_SET_FROZEN_SHAREABLE(hash); // Emit optimized code. @@ -5772,7 +5770,7 @@ pm_compile_shareable_constant_literal(rb_iseq_t *iseq, const pm_node_t *node, pm } case PM_HASH_NODE: { const pm_hash_node_t *cast = (const pm_hash_node_t *) node; - VALUE result = rb_hash_new_capa(cast->elements.size); + VALUE result = rb_hash_alloc_fixed_size(rb_cHash, cast->elements.size); for (size_t index = 0; index < cast->elements.size; index++) { const pm_node_t *element = cast->elements.nodes[index]; From aa7e671c50ef896e4d7fe4fed361874323bbb9b5 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 8 Apr 2026 17:40:15 -0400 Subject: [PATCH 04/34] ZJIT: [DOC] induce_breakpoint! behaves differently when ZJIT is on --- zjit.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/zjit.rb b/zjit.rb index ffee0006962fc9..89a4a15cfd5d95 100644 --- a/zjit.rb +++ b/zjit.rb @@ -58,8 +58,6 @@ def induce_side_exit! = nil # A directive for the compiler to emit a breakpoint instruction at the call site of this method. # To show this to ZJIT, say `::RubyVM::ZJIT.induce_breakpoint!` verbatim. # Other forms are too dynamic to detect during compilation. - # - # Actually running this method does nothing, whether ZJIT sees the call or not. def induce_breakpoint! = nil # Check if `--zjit-stats` is used From b53186a4bf58f6cb07b32a417002e415f08e9fe4 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 8 Apr 2026 17:45:04 -0400 Subject: [PATCH 05/34] ZJIT: Load immediate into register before masking (#16677) This otherwise causes a crash on e.g. https://github.com/ruby/ruby/pull/15220: thread '' (18071) panicked at zjit/src/codegen.rs:2511:21: internal error: entered unreachable code: with_num_bits should not be used for: Value(VALUE(19239180)) stack backtrace: 0: __rustc::rust_begin_unwind at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/std/src/panicking.rs:689:5 1: core::panicking::panic_fmt at /rustc/e408947bfd200af42db322daf0fadfe7e26d3bd1/library/core/src/panicking.rs:80:14 2: zjit::backend::lir::Opnd::with_num_bits at /home/runner/work/ruby/ruby/src/zjit/src/backend/lir.rs:457:18 3: zjit::codegen::gen_guard_type at /home/runner/work/ruby/ruby/src/zjit/src/codegen.rs:2511:21 4: zjit::codegen::gen_insn at /home/runner/work/ruby/ruby/src/zjit/src/codegen.rs:690:55 --- zjit/src/codegen.rs | 19 +++++++++++++++---- zjit/src/codegen_tests.rs | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 5df020f4f56bc6..da15d30d03c741 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -2458,8 +2458,9 @@ fn gen_has_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, ty: Typ asm.csel_e(Opnd::Imm(1), Opnd::Imm(0)) } else if ty.is_subtype(types::StaticSymbol) { // Static symbols have (val & 0xff) == RUBY_SYMBOL_FLAG - // Use 8-bit comparison like YJIT does. GuardType should not be used - // for a known VALUE, which with_num_bits() does not support. + // Use 8-bit comparison like YJIT does. + // If `val` is a constant (rare but possible), put it in a register to allow masking. + let val = asm.load_imm(val); asm.cmp(val.with_num_bits(8), Opnd::UImm(RUBY_SYMBOL_FLAG as u64)); asm.csel_e(Opnd::Imm(1), Opnd::Imm(0)) } else if ty.is_subtype(types::NilClass) { @@ -2528,8 +2529,9 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, guard asm.jne(jit, side_exit(jit, state, GuardType(guard_type))); } else if guard_type.is_subtype(types::StaticSymbol) { // Static symbols have (val & 0xff) == RUBY_SYMBOL_FLAG - // Use 8-bit comparison like YJIT does. GuardType should not be used - // for a known VALUE, which with_num_bits() does not support. + // Use 8-bit comparison like YJIT does. + // If `val` is a constant (rare but possible), put it in a register to allow masking. + let val = asm.load_imm(val); asm.cmp(val.with_num_bits(8), Opnd::UImm(RUBY_SYMBOL_FLAG as u64)); asm.jne(jit, side_exit(jit, state, GuardType(guard_type))); } else if guard_type.is_subtype(types::NilClass) { @@ -3543,6 +3545,15 @@ impl Assembler { } } + /// Emits a load for constant based operands and returns a vreg, + /// otherwise returns recv. + fn load_imm(&mut self, recv: Opnd) -> Opnd { + match recv { + Opnd::Value { .. } | Opnd::UImm(_) | Opnd::Imm(_) => self.load(recv), + _ => recv, + } + } + /// Make a C call while marking the start and end positions for IseqCall fn ccall_with_iseq_call(&mut self, fptr: *const u8, opnds: Vec, iseq_call: &IseqCallRef) -> Opnd { // We need to create our own branch rc objects so that we can move the closure below diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index d57efdc698c031..e19d365057b87b 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -5586,3 +5586,27 @@ fn test_send_block_unused_warning_emitted_from_jit() { test "#), @"true"); } + +#[test] +fn test_load_immediates_into_registers_before_masking() { + // See https://github.com/ruby/ruby/pull/16669 -- this is a reduced reproduction from a Ruby + // spec. + set_call_threshold(2); + assert_snapshot!(inspect(r#" + def test + klass = Class.new do + def ===(o) + true + end + end + + case 1 + when klass.new + :called + end == :called + end + + test + test + "#), @"true"); +} From b24320958b111327c232fde20448a5f6b30b8c02 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 8 Apr 2026 10:47:09 -0400 Subject: [PATCH 06/34] ZJIT: Require Ruby 3.4+ in bisect script --- tool/zjit_bisect.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tool/zjit_bisect.rb b/tool/zjit_bisect.rb index 9fa98ca0f3e162..38235561ecf083 100755 --- a/tool/zjit_bisect.rb +++ b/tool/zjit_bisect.rb @@ -5,6 +5,9 @@ require 'tempfile' require 'timeout' +required_ruby_version = Gem::Version.new("3.4.0") +raise "Ruby version #{required_ruby_version} or higher is required" if Gem::Version.new(RUBY_VERSION) < required_ruby_version + ARGS = {timeout: 5} OptionParser.new do |opts| opts.banner += " -- " From 64a49051a46d986b4d3b1f279eaef3d141e20d5a Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 8 Apr 2026 10:47:34 -0400 Subject: [PATCH 07/34] ZJIT: Detect bad configuration in make command in bisect script --- tool/zjit_bisect.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tool/zjit_bisect.rb b/tool/zjit_bisect.rb index 38235561ecf083..798b98b1f71196 100755 --- a/tool/zjit_bisect.rb +++ b/tool/zjit_bisect.rb @@ -82,6 +82,9 @@ def add_zjit_options cmd zjit_opts = cmd.select { |arg| arg.start_with?("--zjit") } run_opts_index = cmd.find_index { |arg| arg.start_with?("RUN_OPTS=") } specopts_index = cmd.find_index { |arg| arg.start_with?("SPECOPTS=") } + if run_opts_index && specopts_index + raise "Expected only one of RUN_OPTS or SPECOPTS to be present in make command, but both were found" + end if run_opts_index run_opts = Shellwords.split(cmd[run_opts_index].delete_prefix("RUN_OPTS=")) run_opts.concat(zjit_opts) From 9b6066c44176ee78496ec7f6feeea9d26cf6fc8f Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 8 Apr 2026 10:47:59 -0400 Subject: [PATCH 08/34] ZJIT: Suggest correct command in bisect script --- tool/zjit_bisect.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/zjit_bisect.rb b/tool/zjit_bisect.rb index 798b98b1f71196..a265a3c01f9a9b 100755 --- a/tool/zjit_bisect.rb +++ b/tool/zjit_bisect.rb @@ -135,7 +135,7 @@ def run_with_jit_list(ruby, options, jit_list) # Try running with no JIT list to get a stable baseline unless run_with_jit_list(RUBY, OPTIONS, []).success? - cmd = [RUBY, "--zjit-allowed-iseqs=/dev/null", *OPTIONS].shelljoin + cmd = add_zjit_options([RUBY, "--zjit-allowed-iseqs=/dev/null", *OPTIONS]).shelljoin raise "The command failed unexpectedly with an empty JIT list. To reproduce, try running the following: `#{cmd}`" end # Collect the JIT list from the failing Ruby process From ddb60fe38a3e3af3977ca2b3c6f76b5b16c46dbd Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 8 Apr 2026 11:14:55 -0400 Subject: [PATCH 09/34] ZJIT: Guard that an array is not frozen before modifying it ArrayPush calls out to the fast-path, not checking for frozen-ness. In debug mode, this leads to crashes. In release mode, silent erroneous modifications. --- zjit/src/cruby_methods.rs | 1 + zjit/src/hir.rs | 1 + zjit/src/hir/opt_tests.rs | 6 ++++++ zjit/src/hir/tests.rs | 10 ++++++++-- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 767f6499e80606..fa57df96db29d5 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -397,6 +397,7 @@ fn inline_array_push(fun: &mut hir::Function, block: hir::BlockId, recv: hir::In if let &[val] = args { if !fun.likely_a(recv, types::Array, state) { return None; } let recv = fun.coerce_to(block, recv, types::Array, state); + fun.guard_not_frozen(block, recv, state); let _ = fun.push_insn(block, hir::Insn::ArrayPush { array: recv, val, state }); return Some(recv); } diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 9a09e8789cea97..dced5ebe4a117c 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -7111,6 +7111,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let count = get_arg(pc, 0).as_usize(); let vals = state.stack_pop_n(count)?; let array = state.stack_pop()?; + fun.guard_not_frozen(block, array, exit_id); for val in vals.into_iter() { fun.push_insn(block, Insn::ArrayPush { array, val, state: exit_id }); } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 88ab8d30711147..4f457be5f07b79 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -9564,6 +9564,8 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, <<@0x1010, cme:0x1018) v27:ArrayExact = GuardType v10, ArrayExact + v28:CUInt64 = LoadField v27, :_rbasic_flags@0x1040 + v29:CUInt64 = GuardNoBitsSet v28, RUBY_FL_FREEZE=CUInt64(2048) ArrayPush v27, v15 CheckInterrupts Return v27 @@ -9596,6 +9598,8 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, push@0x1010, cme:0x1018) v26:ArrayExact = GuardType v10, ArrayExact + v27:CUInt64 = LoadField v26, :_rbasic_flags@0x1040 + v28:CUInt64 = GuardNoBitsSet v27, RUBY_FL_FREEZE=CUInt64(2048) ArrayPush v26, v15 CheckInterrupts Return v26 @@ -9666,6 +9670,8 @@ mod hir_opt_tests { v26:RubyValue = LoadField v23, :_ep_specval@0x1050 v27:FalseClass = GuardBitEquals v26, Value(false) v28:Array = GuardType v9, Array + v29:CUInt64 = LoadField v28, :_rbasic_flags@0x1051 + v30:CUInt64 = GuardNoBitsSet v29, RUBY_FL_FREEZE=CUInt64(2048) ArrayPush v28, v10 CheckInterrupts Return v28 diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 3358d7573593b7..4cb01d02c89635 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2405,10 +2405,12 @@ pub(crate) mod hir_build_tests { bb3(v9:BasicObject, v10:BasicObject): v16:ArrayExact = ToNewArray v10 v18:Fixnum[1] = Const Value(1) + v20:CUInt64 = LoadField v16, :_rbasic_flags@0x1001 + v21:CUInt64 = GuardNoBitsSet v20, RUBY_FL_FREEZE=CUInt64(2048) ArrayPush v16, v18 - v22:BasicObject = Send v9, :foo, v16 # SendFallbackReason: Uncategorized(opt_send_without_block) + v24:BasicObject = Send v9, :foo, v16 # SendFallbackReason: Uncategorized(opt_send_without_block) CheckInterrupts - Return v22 + Return v24 "); } @@ -4077,6 +4079,8 @@ pub(crate) mod hir_build_tests { bb3(v9:BasicObject, v10:BasicObject): v15:ArrayExact = ToNewArray v10 v17:Fixnum[1] = Const Value(1) + v19:CUInt64 = LoadField v15, :_rbasic_flags@0x1001 + v20:CUInt64 = GuardNoBitsSet v19, RUBY_FL_FREEZE=CUInt64(2048) ArrayPush v15, v17 CheckInterrupts Return v15 @@ -4107,6 +4111,8 @@ pub(crate) mod hir_build_tests { v17:Fixnum[1] = Const Value(1) v19:Fixnum[2] = Const Value(2) v21:Fixnum[3] = Const Value(3) + v23:CUInt64 = LoadField v15, :_rbasic_flags@0x1001 + v24:CUInt64 = GuardNoBitsSet v23, RUBY_FL_FREEZE=CUInt64(2048) ArrayPush v15, v17 ArrayPush v15, v19 ArrayPush v15, v21 From 6ccabf1e82600c3e40c4ddc2eb8045953281c578 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 8 Apr 2026 11:17:41 -0400 Subject: [PATCH 10/34] ZJIT: Guard an array is not frozen before popping from it --- zjit/src/cruby_methods.rs | 1 + zjit/src/hir/opt_tests.rs | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index fa57df96db29d5..e104a0f320a505 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -409,6 +409,7 @@ fn inline_array_pop(fun: &mut hir::Function, block: hir::BlockId, recv: hir::Ins let &[] = args else { return None; }; if !fun.likely_a(recv, types::Array, state) { return None; } let recv = fun.coerce_to(block, recv, types::Array, state); + fun.guard_not_frozen(block, recv, state); fun.guard_not_shared(block, recv, state); Some(fun.push_insn(block, hir::Insn::ArrayPop { array: recv, state })) } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 4f457be5f07b79..71da70d2f39b77 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -9704,17 +9704,18 @@ mod hir_opt_tests { v20:CallableMethodEntry[VALUE(0x1040)] = GuardBitEquals v19, Value(VALUE(0x1040)) v21:RubyValue = LoadField v18, :_ep_specval@0x1048 v22:FalseClass = GuardBitEquals v21, Value(false) - v28:CPtr = GetEP 0 - v29:RubyValue = LoadField v28, :_ep_method_entry@0x1038 - v30:CallableMethodEntry[VALUE(0x1040)] = GuardBitEquals v29, Value(VALUE(0x1040)) - v31:RubyValue = LoadField v28, :_ep_specval@0x1048 - v32:FalseClass = GuardBitEquals v31, Value(false) + v30:CPtr = GetEP 0 + v31:RubyValue = LoadField v30, :_ep_method_entry@0x1038 + v32:CallableMethodEntry[VALUE(0x1040)] = GuardBitEquals v31, Value(VALUE(0x1040)) + v33:RubyValue = LoadField v30, :_ep_specval@0x1048 + v34:FalseClass = GuardBitEquals v33, Value(false) v23:Array = GuardType v6, Array v24:CUInt64 = LoadField v23, :_rbasic_flags@0x1049 - v25:CUInt64 = GuardNoBitsSet v24, RUBY_ELTS_SHARED=CUInt64(4096) - v26:BasicObject = ArrayPop v23 + v25:CUInt64 = GuardNoBitsSet v24, RUBY_FL_FREEZE=CUInt64(2048) + v27:CUInt64 = GuardNoBitsSet v24, RUBY_ELTS_SHARED=CUInt64(4096) + v28:BasicObject = ArrayPop v23 CheckInterrupts - Return v26 + Return v28 "); } From a4fa75ee73d0b017a6ac3879844d42fadd2760db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 02:10:14 +0000 Subject: [PATCH 11/34] Bump taiki-e/install-action Bumps the github-actions group with 1 update in the / directory: [taiki-e/install-action](https://github.com/taiki-e/install-action). Updates `taiki-e/install-action` from 2.75.0 to 2.75.1 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/cf39a74df4a72510be4e5b63348d61067f11e64a...80e6af7a2ec7f280fffe2d0a9d3a12a9d11d86e9) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.75.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/zjit-macos.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index c80dec1b7eaa50..2e736673991aea 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -93,7 +93,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@cf39a74df4a72510be4e5b63348d61067f11e64a # v2.75.0 + - uses: taiki-e/install-action@80e6af7a2ec7f280fffe2d0a9d3a12a9d11d86e9 # v2.75.1 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 5c269774179c95..a0adec6652ac1a 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -119,7 +119,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@cf39a74df4a72510be4e5b63348d61067f11e64a # v2.75.0 + - uses: taiki-e/install-action@80e6af7a2ec7f280fffe2d0a9d3a12a9d11d86e9 # v2.75.1 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From f104525c71fbd6338e98050201253f2fe464ec9b Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 28 Jan 2021 23:14:30 +0900 Subject: [PATCH 12/34] mkmf: cpp_command in C++ mode --- lib/mkmf.rb | 16 +++++++++++++++- test/mkmf/test_egrep_cpp.rb | 12 ++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/mkmf.rb b/lib/mkmf.rb index 38a5a15fb5fdd0..8db4647a405790 100644 --- a/lib/mkmf.rb +++ b/lib/mkmf.rb @@ -573,11 +573,16 @@ def cc_command(opt="") conf) end - def cpp_command(outfile, opt="") + def cpp_config(opt) conf = cc_config(opt) if $universal and (arch_flag = conf['ARCH_FLAG']) and !arch_flag.empty? conf['ARCH_FLAG'] = arch_flag.gsub(/(?:\G|\s)-arch\s+\S+/, '') end + conf + end + + def cpp_command(outfile, opt="") + conf = cpp_config(opt) RbConfig::expand("$(CPP) #$INCFLAGS #$CPPFLAGS #$CFLAGS #{opt} #{CONFTEST_C} #{outfile}", conf) end @@ -3037,6 +3042,15 @@ def cc_command(opt="") conf) end + def cpp_command(outfile, opt="") + conf = cpp_config(opt) + cpp = conf['CPP'].sub(/(\A|\s)#{Regexp.quote(conf['CC'])}(?=\z|\s)/) { + "#$1#{conf['CXX']}" + } + RbConfig::expand("#{cpp} #$INCFLAGS #$CPPFLAGS #$CXXFLAGS #{opt} #{CONFTEST_CXX} #{outfile}", + conf) + end + def link_command(ldflags, *opts) conf = link_config(ldflags, *opts) RbConfig::expand(TRY_LINK_CXX.dup, conf) diff --git a/test/mkmf/test_egrep_cpp.rb b/test/mkmf/test_egrep_cpp.rb index 7ac0e6001001fa..2d101403689a27 100644 --- a/test/mkmf/test_egrep_cpp.rb +++ b/test/mkmf/test_egrep_cpp.rb @@ -10,4 +10,16 @@ def test_egrep_cpp def test_not_have_func assert_equal(false, egrep_cpp(/never match/, ""), MKMFLOG) end + + class TestMkmfEgrepCxx < self + def test_cxx_egrep_cpp + assert_equal(true, MakeMakefile["C++"].egrep_cpp(/^ok/, <<~SRC), MKMFLOG) + #ifdef __cplusplus + ok + #else + #error not C++ + #endif + SRC + end + end end From aaa27ee05754b0358ad3f1c7c5b3d56d626cc497 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 9 Apr 2026 10:28:58 +0900 Subject: [PATCH 13/34] mkmf: Redirect egrep command input --- lib/mkmf.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/mkmf.rb b/lib/mkmf.rb index 8db4647a405790..5f7a7d352a30dc 100644 --- a/lib/mkmf.rb +++ b/lib/mkmf.rb @@ -938,13 +938,7 @@ def egrep_cpp(pat, src, opt = "", &b) false else puts(" egrep '#{pat}'") - begin - stdin = $stdin.dup - $stdin.reopen(f) - system("egrep", pat) - ensure - $stdin.reopen(stdin) - end + system("egrep", pat, in: f) end end ensure From f4b5566d0976f55b1fda8cea943283901a3fdc72 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 9 Apr 2026 10:32:30 +0900 Subject: [PATCH 14/34] mkmf: grep all occurrences in cpp output Apple clang aborts if cpp output is closed in middle, and leaves the preprocessed source and reproduction shell script. --- lib/mkmf.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/mkmf.rb b/lib/mkmf.rb index 5f7a7d352a30dc..39ba4030cc29ea 100644 --- a/lib/mkmf.rb +++ b/lib/mkmf.rb @@ -931,11 +931,9 @@ def egrep_cpp(pat, src, opt = "", &b) xpopen(cpp_command('', opt)) do |f| if Regexp === pat puts(" ruby -ne 'print if #{pat.inspect}'") - f.grep(pat) {|l| + !f.grep(pat) {|l| puts "#{f.lineno}: #{l}" - return true - } - false + }.empty? else puts(" egrep '#{pat}'") system("egrep", pat, in: f) From 4644e4f2fafe45e2c49f18bc9712d0f5fff3d341 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 9 Apr 2026 13:16:57 +0900 Subject: [PATCH 15/34] [Bug #21986] Fix location of numeric literal When checking for suffixes, do not flush the numeric literal token even if no suffix is found. --- parse.y | 1 - test/ruby/test_ast.rb | 8 ++++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/parse.y b/parse.y index 2c8be06373263e..a92faab27fad51 100644 --- a/parse.y +++ b/parse.y @@ -8927,7 +8927,6 @@ number_literal_suffix(struct parser_params *p, int mask) } if (!ISASCII(c) || ISALPHA(c) || c == '_') { p->lex.pcur = lastp; - literal_flush(p, p->lex.pcur); return 0; } pushback(p, c); diff --git a/test/ruby/test_ast.rb b/test/ruby/test_ast.rb index 2ba87c60802f75..5d95dcd46f4225 100644 --- a/test/ruby/test_ast.rb +++ b/test/ruby/test_ast.rb @@ -1751,6 +1751,14 @@ def test_negative_numeric_locations assert_locations(node.children.last.locations, [[1, 0, 1, 2]]) end + def test_numeric_location_with_nonsuffix + node = ast_parse("1if true") + assert_locations(node.children.last.children[1].locations, [[1, 0, 1, 1]]) + + node = ast_parse("1q", error_tolerant: true) + assert_locations(node.children.last.locations, [[1, 0, 1, 1]]) + end + private def ast_parse(src, **options) begin From 4b6a467a4a8998dcd50a54f610ccbaebdae279c1 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 9 Apr 2026 14:13:26 +0900 Subject: [PATCH 16/34] mkmf: skip if C++ compiler not found --- test/mkmf/test_egrep_cpp.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/mkmf/test_egrep_cpp.rb b/test/mkmf/test_egrep_cpp.rb index 2d101403689a27..112632496551a5 100644 --- a/test/mkmf/test_egrep_cpp.rb +++ b/test/mkmf/test_egrep_cpp.rb @@ -20,6 +20,8 @@ def test_cxx_egrep_cpp #error not C++ #endif SRC + rescue Errno::ENOENT + omit "C++ compiler not available: #{$!.message}" end end end From c091c186e425b659f7aa88ffeac20a41a27b0582 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 9 Apr 2026 14:19:02 +0900 Subject: [PATCH 17/34] Fix thread leaks Wait for terminated threads to finish. --- ext/socket/lib/socket.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ext/socket/lib/socket.rb b/ext/socket/lib/socket.rb index 36fcceaee96300..465b74964f9b22 100644 --- a/ext/socket/lib/socket.rb +++ b/ext/socket/lib/socket.rb @@ -924,15 +924,11 @@ def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil, end end ensure - hostname_resolution_threads.each do |thread| - thread.exit - end + hostname_resolution_threads.each(&:exit).each(&:join) hostname_resolution_result&.close - connecting_sockets.each_key do |connecting_socket| - connecting_socket.close - end + connecting_sockets.each_key(&:close) end private_class_method :tcp_with_fast_fallback From b94a7ec7c3c190db613bce3d9ae1a8b26822357e Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 9 Apr 2026 15:10:58 +0900 Subject: [PATCH 18/34] Suppress rev-parse error message when out-of-place build --- defs/gmake.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/defs/gmake.mk b/defs/gmake.mk index 718131e937a4ae..fd1d467a46b14a 100644 --- a/defs/gmake.mk +++ b/defs/gmake.mk @@ -434,7 +434,7 @@ ifneq ($(DOT_WAIT),) endif ifeq ($(HAVE_GIT),yes) -REVISION_LATEST := $(shell $(GIT_IN_SRC) rev-parse HEAD) +REVISION_LATEST := $(shell $(GIT_IN_SRC) rev-parse HEAD 2>/dev/null) else REVISION_LATEST := update endif From 03757030d5b1663e68b4f6e6348a7a71b16ed6c8 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 9 Apr 2026 17:34:06 +0900 Subject: [PATCH 19/34] mkmf: check for C++ compiler It may be set to "false" if usable compiler is not found. --- lib/mkmf.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/mkmf.rb b/lib/mkmf.rb index 39ba4030cc29ea..37ee4a70d9bbbb 100644 --- a/lib/mkmf.rb +++ b/lib/mkmf.rb @@ -3030,14 +3030,16 @@ def conftest_source def cc_command(opt="") conf = cc_config(opt) + cxx_command(opt, conf) RbConfig::expand("$(CXX) #$INCFLAGS #$CPPFLAGS #$CXXFLAGS #$ARCH_FLAG #{opt} -c #{CONFTEST_CXX}", conf) end def cpp_command(outfile, opt="") conf = cpp_config(opt) + cxx = cxx_command(opt, conf) cpp = conf['CPP'].sub(/(\A|\s)#{Regexp.quote(conf['CC'])}(?=\z|\s)/) { - "#$1#{conf['CXX']}" + "#$1#{cxx}" } RbConfig::expand("#{cpp} #$INCFLAGS #$CPPFLAGS #$CXXFLAGS #{opt} #{CONFTEST_CXX} #{outfile}", conf) @@ -3048,6 +3050,12 @@ def link_command(ldflags, *opts) RbConfig::expand(TRY_LINK_CXX.dup, conf) end + def cxx_command(opt="", conf = cc_config(opt)) + cxx = conf['CXX'] + raise Errno::ENOENT, "C++ compiler not found" if !cxx or cxx == 'false' + cxx + end + # :startdoc: end From 51a3f0847782095340df5dbc8fb87450dbc1fbe7 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:11:13 +0200 Subject: [PATCH 20/34] [ruby/prism] Reject `return` and similar with block pass argument Same handling as for `yield`. Fixes [Bug #21988] https://github.com/ruby/prism/commit/2dd20183ad --- prism/prism.c | 9 +++++ test/prism/errors/block_pass_return_value.txt | 33 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 test/prism/errors/block_pass_return_value.txt diff --git a/prism/prism.c b/prism/prism.c index 72c49da6f29499..3ae6ca3d7bf78e 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -19685,6 +19685,15 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u PM_PARSER_ERR_TOKEN_FORMAT(parser, &next, PM_ERR_EXPECT_EOL_AFTER_STATEMENT, pm_token_str(next.type)); } } + + // It's possible that we've parsed a block argument through our + // call to parse_arguments. If we found one, we should mark it + // as invalid and destroy it, as we don't have a place for it. + if (arguments.block != NULL) { + pm_parser_err_node(parser, arguments.block, PM_ERR_UNEXPECTED_BLOCK_ARGUMENT); + pm_node_unreference(parser, arguments.block); + arguments.block = NULL; + } } switch (keyword.type) { diff --git a/test/prism/errors/block_pass_return_value.txt b/test/prism/errors/block_pass_return_value.txt new file mode 100644 index 00000000000000..c9d12281d93040 --- /dev/null +++ b/test/prism/errors/block_pass_return_value.txt @@ -0,0 +1,33 @@ +return &b + ^ unexpected '&', expecting end-of-input + ^ unexpected '&', ignoring it + +return(&b) + ^ unexpected '&', ignoring it + ^ unexpected '&', expecting end-of-input + ^ unexpected '&', ignoring it + ^ expected a matching `)` + ^ unexpected '&', expecting end-of-input + ^ unexpected '&', ignoring it + ^ unexpected ')', expecting end-of-input + ^ unexpected ')', ignoring it + +return a, &b + ^~ block argument should not be given + +return(a, &b) + ^~ unexpected write target + ^ unexpected '&', expecting end-of-input + ^ unexpected '&', ignoring it + ^ expected a matching `)` + ^ unexpected '&', expecting end-of-input + ^ unexpected '&', ignoring it + ^ unexpected ')', expecting end-of-input + ^ unexpected ')', ignoring it + +tap { break a, &b } + ^~ block argument should not be given + +tap { next a, &b } + ^~ block argument should not be given + From ce9d6c899fae67d38be44266a483530bd5846d4e Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 31 Mar 2026 12:56:50 +0100 Subject: [PATCH 21/34] Rename SIZE_POOL_COUNT to HEAP_COUNT in tests --- test/ruby/test_gc_compact.rb | 18 +++++++++--------- test/ruby/test_time.rb | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/ruby/test_gc_compact.rb b/test/ruby/test_gc_compact.rb index 46b4a0605e35b4..95a3d031a73f23 100644 --- a/test/ruby/test_gc_compact.rb +++ b/test/ruby/test_gc_compact.rb @@ -207,7 +207,7 @@ def test_updating_references_for_heap_allocated_shared_arrays end def test_updating_references_for_embed_shared_arrays - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; @@ -256,7 +256,7 @@ def test_updating_references_for_heap_allocated_frozen_shared_arrays end def test_updating_references_for_embed_frozen_shared_arrays - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; @@ -284,7 +284,7 @@ def test_updating_references_for_embed_frozen_shared_arrays end def test_moving_arrays_down_heaps - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; @@ -306,7 +306,7 @@ def test_moving_arrays_down_heaps end def test_moving_arrays_up_heaps - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; @@ -330,7 +330,7 @@ def test_moving_arrays_up_heaps end def test_moving_objects_between_heaps - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 60) begin; @@ -362,7 +362,7 @@ def add_ivars end def test_compact_objects_of_varying_sizes - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_ruby_status([], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 10) begin; @@ -378,7 +378,7 @@ def test_compact_objects_of_varying_sizes end def test_moving_strings_up_heaps - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 30) begin; @@ -399,7 +399,7 @@ def test_moving_strings_up_heaps end def test_moving_strings_down_heaps - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~"end;"}", timeout: 30) begin; @@ -419,7 +419,7 @@ def test_moving_strings_down_heaps end def test_moving_hashes_down_heaps - omit if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 # AR and ST hashes are in the same size pool on 32 bit omit unless RbConfig::SIZEOF["uint64_t"] <= RbConfig::SIZEOF["void*"] diff --git a/test/ruby/test_time.rb b/test/ruby/test_time.rb index 595e183f6c44ca..7370a9d7ca3632 100644 --- a/test/ruby/test_time.rb +++ b/test/ruby/test_time.rb @@ -1421,7 +1421,7 @@ def test_memsize # Time objects are common in some code, try to keep them small omit "Time object size test" if /^(?:i.?86|x86_64)-linux/ !~ RUBY_PLATFORM omit "GC is in debug" if GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] > 0 - omit "memsize is not accurate due to using malloc_usable_size" if GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT] == 1 + omit "memsize is not accurate due to using malloc_usable_size" if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 omit "Only run this test on 64-bit" if RbConfig::SIZEOF["void*"] != 8 require 'objspace' From 772bde30e08a6f417224729eb415a45f8d68f864 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 31 Mar 2026 12:57:12 +0100 Subject: [PATCH 22/34] Use sizeof(VALUE) for pointer alignment checks --- gc/default/default.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 295a51d5d6e0cf..3230e80dd58c82 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -1649,7 +1649,7 @@ heap_page_add_freeobj(rb_objspace_t *objspace, struct heap_page *page, VALUE obj /* obj should belong to page */ !(page->start <= (uintptr_t)obj && (uintptr_t)obj < ((uintptr_t)page->start + (page->total_slots * page->slot_size)) && - obj % pool_slot_sizes[0] == 0)) { + obj % sizeof(VALUE) == 0)) { rb_bug("heap_page_add_freeobj: %p is not rvalue.", (void *)obj); } @@ -2595,7 +2595,7 @@ is_pointer_to_heap(rb_objspace_t *objspace, const void *ptr) if (p < heap_pages_lomem || p > heap_pages_himem) return FALSE; RB_DEBUG_COUNTER_INC(gc_isptr_range); - if (p % pool_slot_sizes[0] != 0) return FALSE; + if (p % sizeof(VALUE) != 0) return FALSE; RB_DEBUG_COUNTER_INC(gc_isptr_align); page = heap_page_for_ptr(objspace, (uintptr_t)ptr); @@ -3501,7 +3501,7 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit do { VALUE vp = (VALUE)p; - GC_ASSERT(vp % pool_slot_sizes[0] == 0); + GC_ASSERT(vp % sizeof(VALUE) == 0); rb_asan_unpoison_object(vp, false); if (bitset & 1) { @@ -5602,7 +5602,7 @@ gc_compact_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t b do { VALUE vp = (VALUE)p; - GC_ASSERT(vp % pool_slot_sizes[0] == 0); + GC_ASSERT(vp % sizeof(VALUE) == 0); if (bitset & 1) { objspace->rcompactor.considered_count_table[BUILTIN_TYPE(vp)]++; From 93710423e13814b8be725d369ed29dac081cb89f Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 31 Mar 2026 12:57:28 +0100 Subject: [PATCH 23/34] Guard rb_obj_embedded_size for zero fields With smaller pool sizes (e.g. 32 bytes), an RObject with 0 embedded fields would request a size smaller than the minimum meaningful object. Ensure at least 1 field worth of space so the embedded size is always large enough for a valid RObject. --- internal/object.h | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/object.h b/internal/object.h index 22da9ddb5e26fc..a6ff4e747639d0 100644 --- a/internal/object.h +++ b/internal/object.h @@ -64,6 +64,7 @@ RBASIC_SET_CLASS(VALUE obj, VALUE klass) static inline size_t rb_obj_embedded_size(uint32_t fields_count) { + if (fields_count < 1) fields_count = 1; return offsetof(struct RObject, as.ary) + (sizeof(VALUE) * fields_count); } #endif /* INTERNAL_OBJECT_H */ From 2567e76ec376fee3b6d6e98e9578bcbb44e23034 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 31 Mar 2026 12:57:56 +0100 Subject: [PATCH 24/34] Handle small pools in shape capacity calculation When pool slot sizes can be smaller than sizeof(struct RBasic) (e.g. a 32-byte pool on 64-bit where RBasic is 16 bytes), the capacity calculation would underflow. Guard against this by setting capacity to 0 for pools too small to hold fields. --- shape.c | 14 ++++++++++---- shape.h | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/shape.c b/shape.c index 90036722f10026..93ccd3eb595fdd 100644 --- a/shape.c +++ b/shape.c @@ -477,14 +477,14 @@ static attr_index_t shape_grow_capa(attr_index_t current_capa) { const attr_index_t *capacities = rb_shape_tree.capacities; + size_t heaps_count = rb_shape_tree.heaps_count; // First try to use the next size that will be embeddable in a larger object slot. - attr_index_t capa; - while ((capa = *capacities)) { + for (size_t i = 0; i < heaps_count; i++) { + attr_index_t capa = capacities[i]; if (capa > current_capa) { return capa; } - capacities++; } return (attr_index_t)rb_malloc_grow_capa(current_capa, sizeof(VALUE)); @@ -1543,8 +1543,14 @@ Init_default_shapes(void) capacities[heaps_count] = 0; size_t index; for (index = 0; index < heaps_count; index++) { - capacities[index] = (heap_sizes[index] - sizeof(struct RBasic)) / sizeof(VALUE); + if (heap_sizes[index] > sizeof(struct RBasic)) { + capacities[index] = (heap_sizes[index] - sizeof(struct RBasic)) / sizeof(VALUE); + } + else { + capacities[index] = 0; + } } + rb_shape_tree.heaps_count = heaps_count; rb_shape_tree.capacities = capacities; #ifdef HAVE_MMAP diff --git a/shape.h b/shape.h index ad9b148247b32e..09487006bc912b 100644 --- a/shape.h +++ b/shape.h @@ -115,6 +115,7 @@ typedef struct { rb_shape_t *shape_list; rb_shape_t *root_shape; const attr_index_t *capacities; + size_t heaps_count; rb_atomic_t next_shape_id; redblack_node_t *shape_cache; From c9b70883640e59bdd1f2f6a538957119cb558fac Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 31 Mar 2026 13:02:18 +0100 Subject: [PATCH 25/34] Introduce power-of-two size pools Replace the RVALUE_SLOT_SIZE-multiplier based pool sizes with explicit power-of-two (and near-power-of-two) slot sizes. On 64-bit this gives 12 heaps (32, 40, 64, 80, 96, 128, 160, 256, 512, 640, 768, 1024) instead of 5, providing finer granularity and less internal fragmentation. On 32-bit the layout is 5 heaps (32, 64, 128, 256, 512). --- gc.rb | 35 +++++++----- gc/default/default.c | 84 ++++++++++++++++++++--------- gc/mmtk/mmtk.c | 18 ++++--- internal/class.h | 4 +- test/.excludes-mmtk/TestObjSpace.rb | 1 - 5 files changed, 96 insertions(+), 46 deletions(-) diff --git a/gc.rb b/gc.rb index 01d798addb1596..48bed27880f3ae 100644 --- a/gc.rb +++ b/gc.rb @@ -269,7 +269,16 @@ def self.stat hash_or_key = nil # GC.stat_heap # # => # {0 => - # {slot_size: 40, + # {slot_size: 32, + # heap_eden_pages: 24, + # heap_eden_slots: 12288, + # total_allocated_pages: 24, + # force_major_gc_count: 0, + # force_incremental_marking_finish_count: 0, + # total_allocated_objects: 8450, + # total_freed_objects: 3120}, + # 1 => + # {slot_size: 64, # heap_eden_pages: 246, # heap_eden_slots: 402802, # total_allocated_pages: 246, @@ -277,8 +286,8 @@ def self.stat hash_or_key = nil # force_incremental_marking_finish_count: 1, # total_allocated_objects: 33867152, # total_freed_objects: 33520523}, - # 1 => - # {slot_size: 80, + # 2 => + # {slot_size: 128, # heap_eden_pages: 84, # heap_eden_slots: 68746, # total_allocated_pages: 84, @@ -286,8 +295,8 @@ def self.stat hash_or_key = nil # force_incremental_marking_finish_count: 4, # total_allocated_objects: 147491, # total_freed_objects: 90699}, - # 2 => - # {slot_size: 160, + # 3 => + # {slot_size: 256, # heap_eden_pages: 157, # heap_eden_slots: 64182, # total_allocated_pages: 157, @@ -295,8 +304,8 @@ def self.stat hash_or_key = nil # force_incremental_marking_finish_count: 0, # total_allocated_objects: 211460, # total_freed_objects: 190075}, - # 3 => - # {slot_size: 320, + # 4 => + # {slot_size: 512, # heap_eden_pages: 8, # heap_eden_slots: 1631, # total_allocated_pages: 8, @@ -304,8 +313,8 @@ def self.stat hash_or_key = nil # force_incremental_marking_finish_count: 0, # total_allocated_objects: 1422, # total_freed_objects: 700}, - # 4 => - # {slot_size: 640, + # 5 => + # {slot_size: 1024, # heap_eden_pages: 16, # heap_eden_slots: 1628, # total_allocated_pages: 16, @@ -316,7 +325,7 @@ def self.stat hash_or_key = nil # # In the example above, the keys in the outer hash are the heap identifiers: # - # GC.stat_heap.keys # => [0, 1, 2, 3, 4] + # GC.stat_heap.keys # => [0, 1, 2, 3, 4, 5] # # On CRuby, each heap identifier is an integer; # on other implementations, a heap identifier may be a string. @@ -324,9 +333,9 @@ def self.stat hash_or_key = nil # With only argument +heap_id+ given, # returns statistics for the given heap identifier: # - # GC.stat_heap(2) + # GC.stat_heap(3) # # => - # {slot_size: 160, + # {slot_size: 256, # heap_eden_pages: 157, # heap_eden_slots: 64182, # total_allocated_pages: 157, @@ -338,7 +347,7 @@ def self.stat hash_or_key = nil # With arguments +heap_id+ and +key+ given, # returns the value for the given key in the given heap: # - # GC.stat_heap(2, :slot_size) # => 160 + # GC.stat_heap(3, :slot_size) # => 256 # # With arguments +nil+ and +hash+ given, # merges the statistics for all heaps into the given hash: diff --git a/gc/default/default.c b/gc/default/default.c index 3230e80dd58c82..1338d9f1f010a7 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -187,9 +187,41 @@ static RB_THREAD_LOCAL_SPECIFIER int malloc_increase_local; #define USE_TICK_T (PRINT_ENTER_EXIT_TICK || PRINT_ROOT_TICKS) #ifndef HEAP_COUNT -# define HEAP_COUNT 5 +# if SIZEOF_VALUE >= 8 +# define HEAP_COUNT 12 +# else +# define HEAP_COUNT 5 +# endif #endif +/* Precomputed reciprocals for fast slot index calculation. + * For slot size d: reciprocal = ceil(2^48 / d). + * Then offset / d == (uint32_t)((offset * reciprocal) >> 48) + * for all offset < HEAP_PAGE_SIZE. */ +#define SLOT_RECIPROCAL_SHIFT 48 + +static const uint64_t heap_slot_reciprocal_table[HEAP_COUNT] = { +#if SIZEOF_VALUE >= 8 + /* 32 */ (1ULL << 48) / 32, + /* 40 */ (1ULL << 48) / 40 + 1, + /* 64 */ (1ULL << 48) / 64, + /* 80 */ (1ULL << 48) / 80 + 1, + /* 96 */ (1ULL << 48) / 96 + 1, + /* 128 */ (1ULL << 48) / 128, + /* 160 */ (1ULL << 48) / 160 + 1, + /* 256 */ (1ULL << 48) / 256, + /* 512 */ (1ULL << 48) / 512, + /* 640 */ (1ULL << 48) / 640 + 1, + /* 768 */ (1ULL << 48) / 768 + 1, + /* 1024 */ (1ULL << 48) / 1024, +#else + /* 32 */ (1ULL << 48) / 32, + /* 64 */ (1ULL << 48) / 64, + /* 128 */ (1ULL << 48) / 128, + /* 256 */ (1ULL << 48) / 256, + /* 512 */ (1ULL << 48) / 512, +#endif +}; typedef struct ractor_newobj_heap_cache { struct free_slot *freelist; struct heap_page *using_page; @@ -689,15 +721,17 @@ size_t rb_gc_impl_obj_slot_size(VALUE obj); #define RVALUE_SLOT_SIZE (sizeof(struct RBasic) + sizeof(VALUE[RBIMPL_RVALUE_EMBED_LEN_MAX]) + RVALUE_OVERHEAD) +#if SIZEOF_VALUE >= 8 static const size_t pool_slot_sizes[HEAP_COUNT] = { - RVALUE_SLOT_SIZE, - RVALUE_SLOT_SIZE * 2, - RVALUE_SLOT_SIZE * 4, - RVALUE_SLOT_SIZE * 8, - RVALUE_SLOT_SIZE * 16, + 32, 40, 64, 80, 96, 128, 160, 256, 512, 640, 768, 1024, }; - -static uint8_t size_to_heap_idx[RVALUE_SLOT_SIZE * (1 << (HEAP_COUNT - 1)) / 8 + 1]; +static uint8_t size_to_heap_idx[1024 / 8 + 1]; +#else +static const size_t pool_slot_sizes[HEAP_COUNT] = { + 32, 64, 128, 256, 512, +}; +static uint8_t size_to_heap_idx[512 / 8 + 1]; +#endif #ifndef MAX # define MAX(a, b) (((a) > (b)) ? (a) : (b)) @@ -707,11 +741,12 @@ static uint8_t size_to_heap_idx[RVALUE_SLOT_SIZE * (1 << (HEAP_COUNT - 1)) / 8 + #endif #define roomof(x, y) (((x) + (y) - 1) / (y)) #define CEILDIV(i, mod) roomof(i, mod) +#define MIN_POOL_SLOT_SIZE 32 enum { HEAP_PAGE_ALIGN = (1UL << HEAP_PAGE_ALIGN_LOG), HEAP_PAGE_ALIGN_MASK = (~(~0UL << HEAP_PAGE_ALIGN_LOG)), HEAP_PAGE_SIZE = HEAP_PAGE_ALIGN, - HEAP_PAGE_BITMAP_LIMIT = CEILDIV(CEILDIV(HEAP_PAGE_SIZE, RVALUE_SLOT_SIZE), BITS_BITLENGTH), + HEAP_PAGE_BITMAP_LIMIT = CEILDIV(CEILDIV(HEAP_PAGE_SIZE, MIN_POOL_SLOT_SIZE), BITS_BITLENGTH), HEAP_PAGE_BITMAP_SIZE = (BITS_SIZE * HEAP_PAGE_BITMAP_LIMIT), }; #define HEAP_PAGE_ALIGN (1 << HEAP_PAGE_ALIGN_LOG) @@ -773,8 +808,11 @@ struct free_slot { }; struct heap_page { + /* Cache line 0: allocation fast path + SLOT_INDEX */ + struct free_slot *freelist; + uintptr_t start; + uint64_t slot_size_reciprocal; unsigned short slot_size; - uint32_t slot_div_magic; unsigned short total_slots; unsigned short free_slots; unsigned short final_slots; @@ -789,8 +827,6 @@ struct heap_page { struct heap_page *free_next; struct heap_page_body *body; - uintptr_t start; - struct free_slot *freelist; struct ccan_list_node page_node; bits_t wb_unprotected_bits[HEAP_PAGE_BITMAP_LIMIT]; @@ -851,15 +887,13 @@ heap_page_in_global_empty_pages_pool(rb_objspace_t *objspace, struct heap_page * #define GET_PAGE_HEADER(x) (&GET_PAGE_BODY(x)->header) #define GET_HEAP_PAGE(x) (GET_PAGE_HEADER(x)->page) -static uint32_t slot_div_magics[HEAP_COUNT]; - static inline size_t -slot_index_for_offset(size_t offset, uint32_t div_magic) +slot_index_for_offset(size_t offset, uint64_t reciprocal) { - return (size_t)(((uint64_t)offset * div_magic) >> 32); + return (uint32_t)(((uint64_t)offset * reciprocal) >> SLOT_RECIPROCAL_SHIFT); } -#define SLOT_INDEX(page, p) slot_index_for_offset((uintptr_t)(p) - (page)->start, (page)->slot_div_magic) +#define SLOT_INDEX(page, p) slot_index_for_offset((uintptr_t)(p) - (page)->start, (page)->slot_size_reciprocal) #define SLOT_BITMAP_INDEX(page, p) (SLOT_INDEX(page, p) / BITS_BITLENGTH) #define SLOT_BITMAP_OFFSET(page, p) (SLOT_INDEX(page, p) & (BITS_BITLENGTH - 1)) #define SLOT_BITMAP_BIT(page, p) ((bits_t)1 << SLOT_BITMAP_OFFSET(page, p)) @@ -1990,19 +2024,17 @@ heap_add_page(rb_objspace_t *objspace, rb_heap_t *heap, struct heap_page *page) GC_ASSERT(!heap->sweeping_page); GC_ASSERT(heap_page_in_global_empty_pages_pool(objspace, page)); - /* Align start to the first slot_size boundary after the page header */ + /* Align start to slot_size boundary */ uintptr_t start = (uintptr_t)page->body + sizeof(struct heap_page_header); - size_t remainder = start % heap->slot_size; - if (remainder != 0) { - start += heap->slot_size - remainder; - } + uintptr_t rem = start % heap->slot_size; + if (rem) start += heap->slot_size - rem; int slot_count = (int)((HEAP_PAGE_SIZE - (start - (uintptr_t)page->body))/heap->slot_size); page->start = start; page->total_slots = slot_count; page->slot_size = heap->slot_size; - page->slot_div_magic = slot_div_magics[heap - heaps]; + page->slot_size_reciprocal = heap_slot_reciprocal_table[heap - heaps]; page->heap = heap; memset(&page->wb_unprotected_bits[0], 0, HEAP_PAGE_BITMAP_SIZE); @@ -9521,11 +9553,15 @@ rb_gc_impl_objspace_init(void *objspace_ptr) rb_bug("Could not preregister postponed job for GC"); } + /* A standard RVALUE (RBasic + embedded VALUEs + debug overhead) must fit + * in at least one pool. In debug builds RVALUE_OVERHEAD can push this + * beyond the 48-byte pool into the 64-byte pool, which is fine. */ + GC_ASSERT(rb_gc_impl_size_allocatable_p(sizeof(struct RBasic) + sizeof(VALUE[RBIMPL_RVALUE_EMBED_LEN_MAX]))); + for (int i = 0; i < HEAP_COUNT; i++) { rb_heap_t *heap = &heaps[i]; heap->slot_size = pool_slot_sizes[i]; - slot_div_magics[i] = (uint32_t)((uint64_t)UINT32_MAX / heap->slot_size + 1); ccan_list_head_init(&heap->pages); } diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index 1dacd95ab5dedc..f83079f3abc850 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -635,12 +635,19 @@ void rb_gc_impl_set_params(void *objspace_ptr) { } static VALUE gc_verify_internal_consistency(VALUE self) { return Qnil; } -#define MMTK_HEAP_COUNT 6 -#define MMTK_MAX_OBJ_SIZE 640 - +#if SIZEOF_VALUE >= 8 +#define MMTK_HEAP_COUNT 12 +#define MMTK_MAX_OBJ_SIZE 1024 static size_t heap_sizes[MMTK_HEAP_COUNT + 1] = { - 32, 40, 80, 160, 320, MMTK_MAX_OBJ_SIZE, 0 + 32, 40, 64, 80, 96, 128, 160, 256, 512, 640, 768, MMTK_MAX_OBJ_SIZE, 0 }; +#else +#define MMTK_HEAP_COUNT 5 +#define MMTK_MAX_OBJ_SIZE 512 +static size_t heap_sizes[MMTK_HEAP_COUNT + 1] = { + 32, 64, 128, 256, MMTK_MAX_OBJ_SIZE, 0 +}; +#endif void rb_gc_impl_init(void) @@ -649,8 +656,7 @@ rb_gc_impl_init(void) rb_hash_aset(gc_constants, ID2SYM(rb_intern("RBASIC_SIZE")), SIZET2NUM(sizeof(struct RBasic))); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVALUE_OVERHEAD")), INT2NUM(0)); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVARGC_MAX_ALLOCATE_SIZE")), LONG2FIX(MMTK_MAX_OBJ_SIZE)); - // Pretend we have 5 size pools - rb_hash_aset(gc_constants, ID2SYM(rb_intern("SIZE_POOL_COUNT")), LONG2FIX(MMTK_HEAP_COUNT)); + rb_hash_aset(gc_constants, ID2SYM(rb_intern("HEAP_COUNT")), LONG2FIX(MMTK_HEAP_COUNT)); // TODO: correctly set RVALUE_OLD_AGE when we have generational GC support rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVALUE_OLD_AGE")), INT2FIX(0)); OBJ_FREEZE(gc_constants); diff --git a/internal/class.h b/internal/class.h index 80bb8cb4f73ceb..c08aa0375524c7 100644 --- a/internal/class.h +++ b/internal/class.h @@ -111,9 +111,9 @@ struct RClass_and_rb_classext_t { }; #if SIZEOF_VALUE >= SIZEOF_LONG_LONG -// Assert that classes can be embedded in heaps[2] (which has 160B slot size) +// Assert that classes can be embedded in heaps[3] (256B slot size on 64-bit). // On 32bit platforms there is no variable width allocation so it doesn't matter. -STATIC_ASSERT(sizeof_rb_classext_t, sizeof(struct RClass_and_rb_classext_t) <= 4 * RVALUE_SIZE); +STATIC_ASSERT(sizeof_rb_classext_t, sizeof(struct RClass_and_rb_classext_t) <= 256); #endif struct RClass_boxable { diff --git a/test/.excludes-mmtk/TestObjSpace.rb b/test/.excludes-mmtk/TestObjSpace.rb index 82858b256fdcc9..94eb2c436d4435 100644 --- a/test/.excludes-mmtk/TestObjSpace.rb +++ b/test/.excludes-mmtk/TestObjSpace.rb @@ -1,5 +1,4 @@ exclude(:test_dump_all_full, "testing behaviour specific to default GC") exclude(:test_dump_flag_age, "testing behaviour specific to default GC") exclude(:test_dump_flags, "testing behaviour specific to default GC") -exclude(:test_dump_includes_slot_size, "can be removed when pool 0 slot size is 32 bytes") exclude(:test_dump_objects_dumps_page_slot_sizes, "testing behaviour specific to default GC") From b6658c1e7838ac4f9ed1ec70eec9789241466ebb Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 31 Mar 2026 13:02:54 +0100 Subject: [PATCH 26/34] Introduce RVALUE_SIZE GC constant Add GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] to store the usable size (excluding debug overhead) of the smallest pool that can hold a standard RVALUE. --- gc/default/default.c | 6 ++++++ gc/mmtk/mmtk.c | 1 + 2 files changed, 7 insertions(+) diff --git a/gc/default/default.c b/gc/default/default.c index 1338d9f1f010a7..477898fc297925 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -9591,6 +9591,12 @@ rb_gc_impl_init(void) { VALUE gc_constants = rb_hash_new(); rb_hash_aset(gc_constants, ID2SYM(rb_intern("DEBUG")), GC_DEBUG ? Qtrue : Qfalse); + /* Minimum slot size that fits a standard RVALUE */ + size_t rvalue_pool = 0; + for (size_t i = 0; i < HEAP_COUNT; i++) { + if (pool_slot_sizes[i] >= RVALUE_SLOT_SIZE) { rvalue_pool = pool_slot_sizes[i]; break; } + } + rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVALUE_SIZE")), SIZET2NUM(rvalue_pool - RVALUE_OVERHEAD)); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RBASIC_SIZE")), SIZET2NUM(sizeof(struct RBasic))); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVALUE_OVERHEAD")), SIZET2NUM(RVALUE_OVERHEAD)); rb_hash_aset(gc_constants, ID2SYM(rb_intern("HEAP_PAGE_BITMAP_SIZE")), SIZET2NUM(HEAP_PAGE_BITMAP_SIZE)); diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index f83079f3abc850..3f680e76f4cd9f 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -653,6 +653,7 @@ void rb_gc_impl_init(void) { VALUE gc_constants = rb_hash_new(); + rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVALUE_SIZE")), SIZET2NUM(SIZEOF_VALUE >= 8 ? 64 : 32)); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RBASIC_SIZE")), SIZET2NUM(sizeof(struct RBasic))); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVALUE_OVERHEAD")), INT2NUM(0)); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVARGC_MAX_ALLOCATE_SIZE")), LONG2FIX(MMTK_MAX_OBJ_SIZE)); From aa5f1922cdebba1a7d47b33bcfe72bf5e39e9734 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 31 Mar 2026 13:03:47 +0100 Subject: [PATCH 27/34] Update tests for new pool layout --- test/objspace/test_objspace.rb | 9 ++++--- test/ruby/test_gc.rb | 4 +-- test/ruby/test_gc_compact.rb | 2 +- test/ruby/test_object.rb | 46 ++++++++++++++++++---------------- test/ruby/test_time.rb | 5 +++- test/ruby/test_transcode.rb | 2 +- zjit/src/hir/opt_tests.rs | 23 ++++++++--------- 7 files changed, 47 insertions(+), 44 deletions(-) diff --git a/test/objspace/test_objspace.rb b/test/objspace/test_objspace.rb index c1208cf4242e84..8d019e587a63e2 100644 --- a/test/objspace/test_objspace.rb +++ b/test/objspace/test_objspace.rb @@ -473,12 +473,12 @@ def test_dump_object assert_include(info, '"embedded":true') assert_include(info, '"ivars":0') - # Non-embed object + # Non-embed object (needs > 6 ivars to exceed pool 0 embed capacity) obj = klass.new - 5.times { |i| obj.instance_variable_set("@ivar#{i}", 0) } + 7.times { |i| obj.instance_variable_set("@ivar#{i}", 0) } info = ObjectSpace.dump(obj) assert_not_include(info, '"embedded":true') - assert_include(info, '"ivars":5') + assert_include(info, '"ivars":7') end def test_dump_control_char @@ -648,7 +648,8 @@ def dump_my_heap_please next if obj["type"] == "SHAPE" assert_not_nil obj["slot_size"] - assert_equal 0, obj["slot_size"] % GC.stat_heap(0, :slot_size) + slot_sizes = GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times.map { |i| GC.stat_heap(i, :slot_size) } + assert_include slot_sizes, obj["slot_size"] } end end diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb index 43540d4412af21..21448294c2073c 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -230,7 +230,7 @@ def test_stat_heap GC.stat(stat) end - assert_equal GC.stat_heap(0, :slot_size) * (2**i), stat_heap[:slot_size] + assert_equal GC.stat_heap(i, :slot_size), stat_heap[:slot_size] assert_operator stat_heap[:heap_live_slots], :<=, stat[:heap_live_slots] assert_operator stat_heap[:heap_free_slots], :<=, stat[:heap_free_slots] assert_operator stat_heap[:heap_final_slots], :<=, stat[:heap_final_slots] @@ -773,7 +773,7 @@ def initialize end def test_gc_stress_at_startup - assert_in_out_err([{"RUBY_DEBUG"=>"gc_stress"}], '', [], [], '[Bug #15784]', success: true, timeout: 60) + assert_in_out_err([{"RUBY_DEBUG"=>"gc_stress"}], '', [], [], '[Bug #15784]', success: true, timeout: 120) end def test_gc_disabled_start diff --git a/test/ruby/test_gc_compact.rb b/test/ruby/test_gc_compact.rb index 95a3d031a73f23..b8d53d71973acc 100644 --- a/test/ruby/test_gc_compact.rb +++ b/test/ruby/test_gc_compact.rb @@ -315,7 +315,7 @@ def test_moving_arrays_up_heaps GC.verify_compaction_references(expand_heap: true, toward: :empty) Fiber.new { - ary = "hello".chars + ary = "hello world".chars # > 6 elements to exceed pool 0 embed capacity $arys = ARY_COUNT.times.map do x = [] ary.each { |e| x << e } diff --git a/test/ruby/test_object.rb b/test/ruby/test_object.rb index 7342b3f933e753..646244a43ae6e0 100644 --- a/test/ruby/test_object.rb +++ b/test/ruby/test_object.rb @@ -358,38 +358,40 @@ def test_remove_instance_variable def test_remove_instance_variable_re_embed assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~'end;'}") begin; - c = Class.new do - attr_reader :a, :b, :c + # Determine the RVALUE pool's embed capacity from GC constants. + rvalue_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] + rbasic_size = GC::INTERNAL_CONSTANTS[:RBASIC_SIZE] + embed_cap = (rvalue_size - rbasic_size) / RbConfig::SIZEOF["void*"] - def initialize - @a = nil - @b = nil - @c = nil - end - end + # Build a class whose initialize sets embed_cap ivars so objects + # are allocated in the RVALUE pool with embedded storage. + init_body = embed_cap.times.map { |i| "@v#{i} = nil" }.join("; ") + c = Class.new { class_eval("def initialize; #{init_body}; end") } o1 = c.new o2 = c.new - o1.instance_variable_set(:@foo, 5) - o1.instance_variable_set(:@a, 0) - o1.instance_variable_set(:@b, 1) - o1.instance_variable_set(:@c, 2) + # All embed_cap ivars fit - should be embedded + embed_cap.times { |i| o1.instance_variable_set(:"@v#{i}", i) } + assert_includes ObjectSpace.dump(o1), '"embedded":true' + + # One more ivar overflows embed capacity + o1.instance_variable_set(:@overflow, 99) refute_includes ObjectSpace.dump(o1), '"embedded":true' - o1.remove_instance_variable(:@foo) + + # Remove the overflow ivar - should re-embed + o1.remove_instance_variable(:@overflow) assert_includes ObjectSpace.dump(o1), '"embedded":true' - o2.instance_variable_set(:@a, 0) - o2.instance_variable_set(:@b, 1) - o2.instance_variable_set(:@c, 2) + # An object that never overflowed is also embedded + embed_cap.times { |i| o2.instance_variable_set(:"@v#{i}", i) } assert_includes ObjectSpace.dump(o2), '"embedded":true' - assert_equal(0, o1.a) - assert_equal(1, o1.b) - assert_equal(2, o1.c) - assert_equal(0, o2.a) - assert_equal(1, o2.b) - assert_equal(2, o2.c) + # Verify values survived re-embedding + embed_cap.times do |i| + assert_equal(i, o1.instance_variable_get(:"@v#{i}")) + assert_equal(i, o2.instance_variable_get(:"@v#{i}")) + end end; end diff --git a/test/ruby/test_time.rb b/test/ruby/test_time.rb index 7370a9d7ca3632..b2cbd06a9f3ab5 100644 --- a/test/ruby/test_time.rb +++ b/test/ruby/test_time.rb @@ -1433,7 +1433,10 @@ def test_memsize RbConfig::SIZEOF["void*"] # Same size as VALUE end sizeof_vtm = RbConfig::SIZEOF["void*"] * 4 + 8 - expect = GC.stat_heap(0, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] + sizeof_timew + sizeof_vtm + data_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] + sizeof_timew + sizeof_vtm + # Round up to the smallest slot size that fits + slot_sizes = GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times.map { |i| GC.stat_heap(i, :slot_size) } + expect = slot_sizes.find { |s| s >= data_size } || slot_sizes.last assert_operator ObjectSpace.memsize_of(t), :<=, expect rescue LoadError => e omit "failed to load objspace: #{e.message}" diff --git a/test/ruby/test_transcode.rb b/test/ruby/test_transcode.rb index 99b5ee8d43da94..2c4462eb717237 100644 --- a/test/ruby/test_transcode.rb +++ b/test/ruby/test_transcode.rb @@ -2344,7 +2344,7 @@ def test_ractor_lazy_load_encoding def test_ractor_lazy_load_encoding_random omit 'unstable on s390x and windows' if RUBY_PLATFORM =~ /s390x|mswin/ - assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}", timeout: 30) begin; rs = [] 100.times do diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 71da70d2f39b77..fa7b045d5f1a38 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -5517,10 +5517,7 @@ mod hir_opt_tests { v14:HeapBasicObject = RefineType v6, HeapBasicObject v17:Fixnum[2] = Const Value(2) PatchPoint SingleRactorMode - StoreField v14, :@bar@0x1004, v17 - WriteBarrier v14, v17 - v40:CShape[0x1005] = Const CShape(0x1005) - StoreField v14, :_shape_id@0x1000, v40 + SetIvar v14, :@bar, v17 CheckInterrupts Return v17 "); @@ -14975,21 +14972,21 @@ mod hir_opt_tests { v14:HeapBasicObject = RefineType v6, HeapBasicObject v17:Fixnum[2] = Const Value(2) PatchPoint SingleRactorMode - StoreField v14, :@b@0x1004, v17 - WriteBarrier v14, v17 - v54:CShape[0x1005] = Const CShape(0x1005) - StoreField v14, :_shape_id@0x1000, v54 + SetIvar v14, :@b, v17 v21:HeapBasicObject = RefineType v14, HeapBasicObject v24:Fixnum[3] = Const Value(3) PatchPoint SingleRactorMode - StoreField v21, :@c@0x1006, v24 - WriteBarrier v21, v24 - v61:CShape[0x1007] = Const CShape(0x1007) - StoreField v21, :_shape_id@0x1000, v61 + SetIvar v21, :@c, v24 v28:HeapBasicObject = RefineType v21, HeapBasicObject v31:Fixnum[4] = Const Value(4) PatchPoint SingleRactorMode - SetIvar v28, :@d, v31 + v50:CShape = LoadField v28, :_shape_id@0x1000 + v51:CShape[0x1004] = GuardBitEquals v50, CShape(0x1004) + v52:CPtr = LoadField v28, :_as_heap@0x1002 + StoreField v52, :@d@0x1005, v31 + WriteBarrier v28, v31 + v55:CShape[0x1006] = Const CShape(0x1006) + StoreField v28, :_shape_id@0x1000, v55 CheckInterrupts Return v31 "); From 5c968c5078f87e539f72712fecefc61f53a6ff29 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 31 Mar 2026 13:04:50 +0100 Subject: [PATCH 28/34] Cache has_sweeping_pages as a bitfield --- gc/default/default.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 477898fc297925..5f008b44a15124 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -685,6 +685,8 @@ typedef struct rb_objspace { unsigned long live_ractor_cache_count; + unsigned int sweeping_heaps; /* bitfield: bit i set while heap i is sweeping */ + int fork_vm_lock_lev; } rb_objspace_t; @@ -1020,12 +1022,7 @@ gc_mode_verify(enum gc_mode mode) static inline bool has_sweeping_pages(rb_objspace_t *objspace) { - for (int i = 0; i < HEAP_COUNT; i++) { - if ((&heaps[i])->sweeping_page) { - return TRUE; - } - } - return FALSE; + return objspace->sweeping_heaps != 0; } static inline size_t @@ -3024,6 +3021,7 @@ gc_abort(void *objspace_ptr) } if (is_lazy_sweeping(objspace)) { + objspace->sweeping_heaps = 0; for (int i = 0; i < HEAP_COUNT; i++) { rb_heap_t *heap = &heaps[i]; @@ -3758,6 +3756,9 @@ static void gc_sweep_start_heap(rb_objspace_t *objspace, rb_heap_t *heap) { heap->sweeping_page = ccan_list_top(&heap->pages, struct heap_page, page_node); + if (heap->sweeping_page) { + objspace->sweeping_heaps |= (1u << (heap - heaps)); + } heap->free_pages = NULL; heap->pooled_pages = NULL; if (!objspace->flags.immediate_sweep) { @@ -3984,6 +3985,7 @@ gc_sweep_step(rb_objspace_t *objspace, rb_heap_t *heap) } while ((sweep_page = heap->sweeping_page)); if (!heap->sweeping_page) { + objspace->sweeping_heaps &= ~(1u << (heap - heaps)); gc_sweep_finish_heap(objspace, heap); if (!has_sweeping_pages(objspace)) { From a8009c9843dcbd742190700b59464e971236a55e Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 31 Mar 2026 13:05:25 +0100 Subject: [PATCH 29/34] Allow flex in heap growth threshold Add a 7/8 multiplier to the min_free_slots checks in gc_sweep_finish_heap and gc_marks_finish, allowing heaps to be up to ~12.5% below the free slots target without triggering a major GC or forced growth. With 12 heaps instead of 5, each heap independently hitting the exact threshold would cause excessive memory growth. The slack prevents cascading growth decisions while still ensuring heaps stay close to their target occupancy. --- gc/default/default.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 5f008b44a15124..7bad68f9029a35 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -3870,7 +3870,8 @@ gc_sweep_finish_heap(rb_objspace_t *objspace, rb_heap_t *heap) heap_allocatable_bytes_expand(objspace, heap, swept_slots, heap->total_slots, heap->slot_size); } } - else if (objspace->heap_pages.allocatable_bytes < (min_free_slots - swept_slots) * heap->slot_size) { + else if (swept_slots < min_free_slots * 7 / 8 && + objspace->heap_pages.allocatable_bytes < (min_free_slots * 7 / 8 - swept_slots) * heap->slot_size) { gc_needs_major_flags |= GPR_FLAG_MAJOR_BY_NOFREE; heap->force_major_gc_count++; } @@ -5500,7 +5501,7 @@ gc_marks_finish(rb_objspace_t *objspace) } if (objspace->heap_pages.allocatable_bytes == 0 && sweep_slots < min_free_slots) { - if (!full_marking) { + if (!full_marking && sweep_slots < min_free_slots * 7 / 8) { if (objspace->profile.count - objspace->rgengc.last_major_gc < RVALUE_OLD_AGE) { full_marking = TRUE; } From 80e3a8d2b246af9391c254f055826f8f53750dc7 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 2 Apr 2026 23:07:33 +0100 Subject: [PATCH 30/34] Fix zjit hir tests --- zjit/src/hir/opt_tests.rs | 57 ++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index fa7b045d5f1a38..96543daf7f9b56 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -7791,22 +7791,23 @@ mod hir_opt_tests { v26:CInt64 = IntAnd v12, v23 v27:CBool = IsBitEqual v26, v25 IfTrue v27, bb6() - v31:BasicObject = GetIvar v11, :@foo - Jump bb4(v31) + v32:BasicObject = GetIvar v11, :@foo + Jump bb4(v32) bb5(): v20:CPtr = LoadField v11, :_as_heap@0x1003 v21:BasicObject = LoadField v20, :@foo@0x1004 Jump bb4(v21) bb6(): - v29:BasicObject = LoadField v11, :@foo@0x1003 - Jump bb4(v29) + v29:CPtr = LoadField v11, :_as_heap@0x1003 + v30:BasicObject = LoadField v29, :@foo@0x1000 + Jump bb4(v30) bb4(v13:BasicObject): - v34:Fixnum[1] = Const Value(1) + v35:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v45:Fixnum = GuardType v13, Fixnum - v46:Fixnum = FixnumAdd v45, v34 + v46:Fixnum = GuardType v13, Fixnum + v47:Fixnum = FixnumAdd v46, v35 CheckInterrupts - Return v46 + Return v47 "); } @@ -7861,30 +7862,32 @@ mod hir_opt_tests { v17:CInt64 = IntAnd v12, v14 v18:CBool = IsBitEqual v17, v16 IfTrue v18, bb5() - v22:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) - v23:CPtr[CPtr(0x1002)] = Const CPtr(0x1002) - v24 = RefineType v23, CUInt64 - v25:CInt64 = IntAnd v12, v22 - v26:CBool = IsBitEqual v25, v24 - IfTrue v26, bb6() - v44:CShape = LoadField v11, :_shape_id@0x1003 - v45:CShape[0x1004] = GuardBitEquals v44, CShape(0x1004) - v46:BasicObject = LoadField v11, :@foo@0x1005 - Jump bb4(v46) + v23:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v24:CPtr[CPtr(0x1002)] = Const CPtr(0x1002) + v25 = RefineType v24, CUInt64 + v26:CInt64 = IntAnd v12, v23 + v27:CBool = IsBitEqual v26, v25 + IfTrue v27, bb6() + v45:CShape = LoadField v11, :_shape_id@0x1003 + v46:CShape[0x1004] = GuardBitEquals v45, CShape(0x1004) + v47:CPtr = LoadField v11, :_as_heap@0x1005 + v48:BasicObject = LoadField v47, :@foo@0x1000 + Jump bb4(v48) bb5(): - v20:BasicObject = LoadField v11, :@foo@0x1005 - Jump bb4(v20) + v20:CPtr = LoadField v11, :_as_heap@0x1005 + v21:BasicObject = LoadField v20, :@foo@0x1000 + Jump bb4(v21) bb6(): - v28:CPtr = LoadField v11, :_as_heap@0x1005 - v29:BasicObject = LoadField v28, :@foo@0x1006 - Jump bb4(v29) + v29:CPtr = LoadField v11, :_as_heap@0x1005 + v30:BasicObject = LoadField v29, :@foo@0x1006 + Jump bb4(v30) bb4(v13:BasicObject): - v34:Fixnum[1] = Const Value(1) + v35:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v49:Fixnum = GuardType v13, Fixnum - v50:Fixnum = FixnumAdd v49, v34 + v51:Fixnum = GuardType v13, Fixnum + v52:Fixnum = FixnumAdd v51, v35 CheckInterrupts - Return v50 + Return v52 "); } From 2fd891f283d78347efb97de8db7ae6d2ff79d297 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Mon, 6 Apr 2026 20:30:06 +0100 Subject: [PATCH 31/34] Use the pre-processor to generate slot sizes and reciprocals --- gc/default/default.c | 46 ++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 7bad68f9029a35..ccb4cc19ffc42e 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -194,33 +194,28 @@ static RB_THREAD_LOCAL_SPECIFIER int malloc_increase_local; # endif #endif +/* The reciprocal table and pool_slot_sizes array are both generated from this + * single definition, so they can never get out of sync. */ +#if SIZEOF_VALUE >= 8 +# define EACH_POOL_SLOT_SIZE(SLOT) \ + SLOT(32) SLOT(40) SLOT(64) SLOT(80) SLOT(96) SLOT(128) \ + SLOT(160) SLOT(256) SLOT(512) SLOT(640) SLOT(768) SLOT(1024) +#else +# define EACH_POOL_SLOT_SIZE(SLOT) \ + SLOT(32) SLOT(64) SLOT(128) SLOT(256) SLOT(512) +#endif + /* Precomputed reciprocals for fast slot index calculation. * For slot size d: reciprocal = ceil(2^48 / d). * Then offset / d == (uint32_t)((offset * reciprocal) >> 48) * for all offset < HEAP_PAGE_SIZE. */ #define SLOT_RECIPROCAL_SHIFT 48 +#define SLOT_RECIPROCAL(size) (((1ULL << SLOT_RECIPROCAL_SHIFT) + (size) - 1) / (size)) static const uint64_t heap_slot_reciprocal_table[HEAP_COUNT] = { -#if SIZEOF_VALUE >= 8 - /* 32 */ (1ULL << 48) / 32, - /* 40 */ (1ULL << 48) / 40 + 1, - /* 64 */ (1ULL << 48) / 64, - /* 80 */ (1ULL << 48) / 80 + 1, - /* 96 */ (1ULL << 48) / 96 + 1, - /* 128 */ (1ULL << 48) / 128, - /* 160 */ (1ULL << 48) / 160 + 1, - /* 256 */ (1ULL << 48) / 256, - /* 512 */ (1ULL << 48) / 512, - /* 640 */ (1ULL << 48) / 640 + 1, - /* 768 */ (1ULL << 48) / 768 + 1, - /* 1024 */ (1ULL << 48) / 1024, -#else - /* 32 */ (1ULL << 48) / 32, - /* 64 */ (1ULL << 48) / 64, - /* 128 */ (1ULL << 48) / 128, - /* 256 */ (1ULL << 48) / 256, - /* 512 */ (1ULL << 48) / 512, -#endif +#define SLOT(size) SLOT_RECIPROCAL(size), + EACH_POOL_SLOT_SIZE(SLOT) +#undef SLOT }; typedef struct ractor_newobj_heap_cache { struct free_slot *freelist; @@ -723,15 +718,16 @@ size_t rb_gc_impl_obj_slot_size(VALUE obj); #define RVALUE_SLOT_SIZE (sizeof(struct RBasic) + sizeof(VALUE[RBIMPL_RVALUE_EMBED_LEN_MAX]) + RVALUE_OVERHEAD) -#if SIZEOF_VALUE >= 8 static const size_t pool_slot_sizes[HEAP_COUNT] = { - 32, 40, 64, 80, 96, 128, 160, 256, 512, 640, 768, 1024, +#define SLOT(size) size, + EACH_POOL_SLOT_SIZE(SLOT) +#undef SLOT }; + + +#if SIZEOF_VALUE >= 8 static uint8_t size_to_heap_idx[1024 / 8 + 1]; #else -static const size_t pool_slot_sizes[HEAP_COUNT] = { - 32, 64, 128, 256, 512, -}; static uint8_t size_to_heap_idx[512 / 8 + 1]; #endif From 5381f0fa56b7d3ee4cb4726c22c778f09c4b4d00 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Mon, 6 Apr 2026 20:56:14 +0100 Subject: [PATCH 32/34] Replace sweeping_heaps map with a counter We implemented some bit twiddling logic with an unsigned int to have a neat way of tracking which heaps were currently sweeping, but we actually don't need to care which heap is sweeping right now, just whether some are or not, so we can replace this with a counter. --- gc/default/default.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index ccb4cc19ffc42e..309f47ac4f4de9 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -680,7 +680,7 @@ typedef struct rb_objspace { unsigned long live_ractor_cache_count; - unsigned int sweeping_heaps; /* bitfield: bit i set while heap i is sweeping */ + int sweeping_heap_count; int fork_vm_lock_lev; } rb_objspace_t; @@ -1018,7 +1018,7 @@ gc_mode_verify(enum gc_mode mode) static inline bool has_sweeping_pages(rb_objspace_t *objspace) { - return objspace->sweeping_heaps != 0; + return objspace->sweeping_heap_count != 0; } static inline size_t @@ -3017,7 +3017,7 @@ gc_abort(void *objspace_ptr) } if (is_lazy_sweeping(objspace)) { - objspace->sweeping_heaps = 0; + objspace->sweeping_heap_count = 0; for (int i = 0; i < HEAP_COUNT; i++) { rb_heap_t *heap = &heaps[i]; @@ -3753,7 +3753,7 @@ gc_sweep_start_heap(rb_objspace_t *objspace, rb_heap_t *heap) { heap->sweeping_page = ccan_list_top(&heap->pages, struct heap_page, page_node); if (heap->sweeping_page) { - objspace->sweeping_heaps |= (1u << (heap - heaps)); + objspace->sweeping_heap_count++; } heap->free_pages = NULL; heap->pooled_pages = NULL; @@ -3982,7 +3982,8 @@ gc_sweep_step(rb_objspace_t *objspace, rb_heap_t *heap) } while ((sweep_page = heap->sweeping_page)); if (!heap->sweeping_page) { - objspace->sweeping_heaps &= ~(1u << (heap - heaps)); + objspace->sweeping_heap_count--; + GC_ASSERT(objspace->sweeping_heap_count >= 0); gc_sweep_finish_heap(objspace, heap); if (!has_sweeping_pages(objspace)) { From 3c28bb53a7447c1e8b88da1ccbae76801d5b643a Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Mon, 6 Apr 2026 22:24:12 +0100 Subject: [PATCH 33/34] Make it obvious that field count guard is for debug This is because when `RVALUE_OVERHEAD` is positive, ie. when `RACTOR_CHECK_MODE` is enabled and we need to store the pointer to the owning ractor, we need to make sure there is enough space to store it. With the previous size pools the smallest size pool was 40 bytes, this gets expanded to 48 bytes when debug mode is on in order to make space for this extra pointer. because rb_obj_embedded_size(0) returns just the header with no field space it wants to be allocated in the 40 byte slot, this gives 16 bytes which is enough for RBasic only, but because this slot is 48 bytes in debug mode, we get the extra space for the pointer. When the smallest slot is 32 bytes it becomes 40 bytes in debug mode, this causes objects with no ivars to to get allocated in this pool because according to this calc it fits. but this doesn't leave the extra word for the ractor pointer. So in debug mode, we'll clamp this to 1 so that there's always enough space for 1 extra field to force allocation into the 40/48 byte pool. --- internal/object.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/object.h b/internal/object.h index a6ff4e747639d0..3cf58d55d940e3 100644 --- a/internal/object.h +++ b/internal/object.h @@ -64,7 +64,9 @@ RBASIC_SET_CLASS(VALUE obj, VALUE klass) static inline size_t rb_obj_embedded_size(uint32_t fields_count) { +#if (defined(RACTOR_CHECK_MODE) && RACTOR_CHECK_MODE) || (defined(GC_DEBUG) && GC_DEBUG) if (fields_count < 1) fields_count = 1; +#endif return offsetof(struct RObject, as.ary) + (sizeof(VALUE) * fields_count); } #endif /* INTERNAL_OBJECT_H */ From c91977801797ef15a97aa2f325c7458ef393166b Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Mon, 6 Apr 2026 22:37:30 +0100 Subject: [PATCH 34/34] Remove extra sentinel from shape capacities This isn't a 0 terminated list anymore because we iterate over heaps_count directly. So we don't need to allocate an extra byte for the sentinel --- shape.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shape.c b/shape.c index 93ccd3eb595fdd..c9169313cffe96 100644 --- a/shape.c +++ b/shape.c @@ -1539,8 +1539,7 @@ Init_default_shapes(void) while (heap_sizes[heaps_count]) { heaps_count++; } - attr_index_t *capacities = ALLOC_N(attr_index_t, heaps_count + 1); - capacities[heaps_count] = 0; + attr_index_t *capacities = ALLOC_N(attr_index_t, heaps_count); size_t index; for (index = 0; index < heaps_count; index++) { if (heap_sizes[index] > sizeof(struct RBasic)) {