From c2473016ed246a5f3a43da0f73ad0db83865b82c Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Mon, 27 Jul 2026 14:35:09 -0400 Subject: [PATCH 1/4] Set STR_DUPLICATE_MAX_EMBED_LEN to 256 I benchmarked various values of STR_DUPLICATE_MAX_EMBED_LEN and the performance impacts. For String#dup, creating a view is faster until it is below 256 bytes. Unsurprisingly, if we add modification to the string, then it is faster to eagerly create a copy than use a view. Surprisingly, if we append to the string, it's still faster to go from embedded -> extended than shared -> extended. However, both of these cases will incur a memory penalty as the remainder of the slot is wasted memory. Benchmark: require "benchmark" TIMES = 10_000_000 Benchmark.bm do |x| [64, 128, 256, 512, 1024].each do |len| len -= 25 # Subtract header length x.report("String#dup for #{len.to_s}") do pid = fork do str = ("a" * len).freeze i = 0 while i < TIMES str.dup i += 1 end end Process.wait(pid) end x.report("String#dup + String#[]= for #{len.to_s}") do pid = fork do str = ("a" * len).freeze i = 0 while i < TIMES dup = str.dup dup[0] = "1" i += 1 end end Process.wait(pid) end x.report("String#dup + String#<< for #{len.to_s}") do pid = fork do str = ("a" * len).freeze i = 0 while i < TIMES dup = str.dup dup << "1" i += 1 end end Process.wait(pid) end end end Result: 1024 user system total real String#dup for 39 0.000085 0.000646 0.207714 ( 0.208041) String#dup + String#[]= for 39 0.000067 0.000379 0.499957 ( 0.500514) String#dup + String#<< for 39 0.000069 0.000413 0.600205 ( 0.600984) String#dup for 103 0.000068 0.000374 0.236938 ( 0.237424) String#dup + String#[]= for 103 0.000063 0.000401 0.501791 ( 0.502506) String#dup + String#<< for 103 0.000066 0.000395 0.600041 ( 0.600870) String#dup for 231 0.000066 0.000370 0.281924 ( 0.282409) String#dup + String#[]= for 231 0.000064 0.000355 0.574779 ( 0.575599) String#dup + String#<< for 231 0.000070 0.000395 0.691011 ( 0.691972) String#dup for 487 0.000070 0.000366 0.355631 ( 0.356230) String#dup + String#[]= for 487 0.000067 0.000391 0.653774 ( 0.654509) String#dup + String#<< for 487 0.000066 0.000359 0.891413 ( 0.892607) String#dup for 999 0.000068 0.000394 0.527350 ( 0.528033) String#dup + String#[]= for 999 0.000067 0.000378 0.786794 ( 0.787682) String#dup + String#<< for 999 0.000063 0.000363 1.070604 ( 1.071885) 512 user system total real String#dup for 39 0.000083 0.000639 0.207305 ( 0.207687) String#dup + String#[]= for 39 0.000067 0.000398 0.501968 ( 0.502645) String#dup + String#<< for 39 0.000071 0.000414 0.586533 ( 0.587365) String#dup for 103 0.000064 0.000397 0.222104 ( 0.222600) String#dup + String#[]= for 103 0.000069 0.000389 0.483612 ( 0.484240) String#dup + String#<< for 103 0.000069 0.000380 0.606659 ( 0.607510) String#dup for 231 0.000067 0.000388 0.292563 ( 0.293070) String#dup + String#[]= for 231 0.000064 0.000389 0.573774 ( 0.574479) String#dup + String#<< for 231 0.000069 0.000413 0.739569 ( 0.740614) String#dup for 487 0.000067 0.000419 0.370516 ( 0.371072) String#dup + String#[]= for 487 0.000060 0.000337 0.676507 ( 0.677282) String#dup + String#<< for 487 0.000074 0.000410 0.911670 ( 0.916480) String#dup for 999 0.000069 0.000397 0.239633 ( 0.240049) String#dup + String#[]= for 999 0.000066 0.000370 1.196397 ( 1.198272) String#dup + String#<< for 999 0.000072 0.000396 1.683562 ( 1.686244) 256 user system total real String#dup for 39 0.000073 0.000588 0.198103 ( 0.198485) String#dup + String#[]= for 39 0.000067 0.000397 0.475345 ( 0.476011) String#dup + String#<< for 39 0.000074 0.000408 0.589001 ( 0.589778) String#dup for 103 0.000073 0.000420 0.244381 ( 0.244818) String#dup + String#[]= for 103 0.000065 0.000421 0.480720 ( 0.481569) String#dup + String#<< for 103 0.000070 0.000414 0.589164 ( 0.590123) String#dup for 231 0.000077 0.000447 0.272169 ( 0.272640) String#dup + String#[]= for 231 0.000067 0.000425 0.517280 ( 0.518037) String#dup + String#<< for 231 0.000064 0.000409 0.681593 ( 0.682572) String#dup for 487 0.000065 0.000390 0.229708 ( 0.230153) String#dup + String#[]= for 487 0.000067 0.000395 0.872966 ( 0.874233) String#dup + String#<< for 487 0.000071 0.000411 1.317758 ( 1.320085) String#dup for 999 0.000070 0.000397 0.255197 ( 0.255609) String#dup + String#[]= for 999 0.000068 0.000380 1.174781 ( 1.176700) String#dup + String#<< for 999 0.000067 0.000389 1.729608 ( 1.738797) 128 user system total real String#dup for 39 0.000105 0.000950 0.205384 ( 0.206565) String#dup + String#[]= for 39 0.000066 0.000406 0.483924 ( 0.485308) String#dup + String#<< for 39 0.000075 0.000433 0.590150 ( 0.591641) String#dup for 103 0.000076 0.000414 0.225900 ( 0.227975) String#dup + String#[]= for 103 0.000098 0.000604 0.499234 ( 0.503926) String#dup + String#<< for 103 0.000070 0.000400 0.619562 ( 0.627411) String#dup for 231 0.000073 0.000445 0.241433 ( 0.243606) String#dup + String#[]= for 231 0.000067 0.000402 0.843665 ( 0.853293) String#dup + String#<< for 231 0.000071 0.000392 0.990903 ( 0.994555) String#dup for 487 0.000073 0.000403 0.234920 ( 0.236132) String#dup + String#[]= for 487 0.000077 0.000445 0.944582 ( 0.956001) String#dup + String#<< for 487 0.000069 0.000410 1.337099 ( 1.341250) String#dup for 999 0.000072 0.000420 0.233520 ( 0.234182) String#dup + String#[]= for 999 0.000072 0.000419 1.186288 ( 1.188972) String#dup + String#<< for 999 0.000069 0.000436 1.685112 ( 1.689536) --- string.c | 6 +++--- test/objspace/test_objspace.rb | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/string.c b/string.c index 33377dc39497d3..ce001d35aaec5d 100644 --- a/string.c +++ b/string.c @@ -1967,10 +1967,10 @@ str_duplicate_setup_heap(VALUE klass, VALUE str, VALUE dup) str_duplicate_setup_encoding(str, dup, flags); } -/* Force duplicated strings above 1024 bytes to be views rather than copies since +/* Force duplicated strings above 256 bytes to be views rather than copies since * copying will use memory and have significant overhead. - * Calculated as: 1024 - header size - NUL terminator size */ -#define STR_DUPLICATE_MAX_EMBED_LEN ((long)(1024 - offsetof(struct RString, as.embed) - 1)) + * Calculated as: 256 - header size - NUL terminator size */ +#define STR_DUPLICATE_MAX_EMBED_LEN ((long)(256 - offsetof(struct RString, as.embed) - 1)) static inline VALUE str_duplicate(VALUE klass, VALUE str) diff --git a/test/objspace/test_objspace.rb b/test/objspace/test_objspace.rb index c96222e7630c3e..cfb38e69750023 100644 --- a/test/objspace/test_objspace.rb +++ b/test/objspace/test_objspace.rb @@ -28,7 +28,7 @@ def test_memsize_of ObjectSpace.memsize_of(//.match(""))) end - STR_DUPLICATE_MAX_EMBED_LEN = 999 # From macro defined in string.c + STR_DUPLICATE_MAX_EMBED_LEN = 256 - (RbConfig::SIZEOF["void*"] * 3) - 1 # From macro defined in string.c def test_memsize_of_root_shared_string a = "a" * (STR_DUPLICATE_MAX_EMBED_LEN + 1) @@ -36,7 +36,7 @@ def test_memsize_of_root_shared_string c = nil ObjectSpace.each_object(String) {|x| break c = x if a == x and x.frozen?} rv_size = Integer(ObjectSpace.dump(a)[/"slot_size":(\d+)/, 1]) - assert_equal([rv_size, rv_size, a.length + 1 + rv_size], [a, b, c].map {|x| ObjectSpace.memsize_of(x)}) + assert_equal([rv_size, GC::INTERNAL_CONSTANTS[:RVALUE_SIZE], rv_size], [a, b, c].map {|x| ObjectSpace.memsize_of(x)}) end def test_argf_memsize From d3aee406de6693a06b87d95fdafa258be1ee1681 Mon Sep 17 00:00:00 2001 From: himura467 Date: Tue, 28 Jul 2026 22:52:44 +0900 Subject: [PATCH 2/4] Raise TypeError for uninitialized Ractor::Port Ractor::Port has a public allocator, so Ractor::Port.allocate returns a port whose owner Ractor is still NULL; only ractor_port_init() fills it in. The methods dereferenced it unchecked: Ractor::Port.allocate.closed? # [BUG] Segmentation fault at 0x60 Route the paths reachable from Ruby through ractor_port_ptr_check(), which raises TypeError as other uninitialized typed data objects do. The self of #initialize_copy keeps the unchecked accessor because it is legitimately uninitialized there; only orig is checked. Ractor.select did not crash, but such a port has id 0, the same as the current Ractor's default port, so it took messages from it. Port#closed? also loses Primitive.attr! :leaf. It can raise now, and it was not a leaf before either, since the foreign Ractor path takes RACTOR_LOCK(). [Bug #22214] --- bootstraptest/test_ractor.rb | 37 ++++++++++++++++++++++++++++++++++++ ractor.rb | 3 +-- ractor_sync.c | 31 +++++++++++++++++++++--------- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index a288cd1149787d..64ce8a91165b3c 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -2715,3 +2715,40 @@ def foo(a:, b:, c:) = super(a: a, b: b, c: c) r.send(nil) r.value } + +# Ractor::Port.allocate leaves the owner Ractor NULL, so every method reachable +# from Ruby must reject such a port instead of dereferencing it. [Bug #22214] +assert_equal 'ok', %q{ + port = Ractor::Port.allocate + + messages = [ + -> { port.send(1) }, + -> { port << 1 }, + -> { port.receive }, + -> { port.close }, + -> { port.closed? }, + -> { port.inspect }, + -> { Ractor::Port.new.__send__(:initialize_copy, port) }, + -> { Ractor.new { :x }.monitor(port) }, + -> { Ractor.new { :x }.unmonitor(port) }, + -> { Ractor.select(port) }, + ].map do |blk| + begin + blk.call + 'not raised' + rescue TypeError => e + e.message + end + end + + messages.uniq == ['uninitialized Ractor::Port'] ? :ok : messages +} + +assert_equal 'uninitialized MyPort', %q{ + class MyPort < Ractor::Port; end + begin + MyPort.allocate.closed? + rescue TypeError => e + e.message + end +} diff --git a/ractor.rb b/ractor.rb index 1168ed7803599d..180825d534ed8d 100644 --- a/ractor.rb +++ b/ractor.rb @@ -825,7 +825,6 @@ def close # # Returns whether or not the port is closed. def closed? - Primitive.attr! :leaf __builtin_cexpr! %q{ ractor_port_closed_p(ec, self); } @@ -836,7 +835,7 @@ def closed? # port.inspect -> string def inspect "#r)))" + __builtin_cexpr! "SIZET2NUM(rb_ractor_id(ractor_port_ptr_check(self)->r))" } id:#{ __builtin_cexpr! "SIZET2NUM(ractor_port_id(RACTOR_PORT_PTR(self)))" }>" diff --git a/ractor_sync.c b/ractor_sync.c index 5b513a1315d831..6ac0c11f632bce 100644 --- a/ractor_sync.c +++ b/ractor_sync.c @@ -53,6 +53,19 @@ RACTOR_PORT_PTR(VALUE self) return RTYPEDDATA_GET_DATA(self); } +// r is NULL between Ractor::Port.allocate and ractor_port_init() +static struct ractor_port * +ractor_port_ptr_check(VALUE self) +{ + struct ractor_port *rp = RACTOR_PORT_PTR(self); + + if (UNLIKELY(rp->r == NULL)) { + rb_raise(rb_eTypeError, "uninitialized %"PRIsVALUE, rb_obj_class(self)); + } + + return rp; +} + static VALUE ractor_port_alloc(VALUE klass) { @@ -94,8 +107,8 @@ ractor_port_initialize(VALUE self) static VALUE ractor_port_initialize_copy(VALUE self, VALUE orig) { - struct ractor_port *dst = RACTOR_PORT_PTR(self); - struct ractor_port *src = RACTOR_PORT_PTR(orig); + struct ractor_port *dst = RACTOR_PORT_PTR(self); // uninitialized by definition + struct ractor_port *src = ractor_port_ptr_check(orig); dst->r = src->r; RB_OBJ_WRITTEN(self, Qundef, dst->r->pub.self); dst->id_ = ractor_port_id(src); @@ -120,7 +133,7 @@ ractor_port_p(VALUE self) static VALUE ractor_port_receive(rb_execution_context_t *ec, VALUE self) { - const struct ractor_port *rp = RACTOR_PORT_PTR(self); + const struct ractor_port *rp = ractor_port_ptr_check(self); if (rp->r != rb_ec_ractor_ptr(ec)) { rb_raise(rb_eRactorError, "only allowed from the creator Ractor of this port"); @@ -134,7 +147,7 @@ ractor_port_receive(rb_execution_context_t *ec, VALUE self) static VALUE ractor_port_send(rb_execution_context_t *ec, VALUE self, VALUE obj, VALUE move) { - const struct ractor_port *rp = RACTOR_PORT_PTR(self); + const struct ractor_port *rp = ractor_port_ptr_check(self); ractor_send(ec, rp, obj, RTEST(move)); RB_GC_GUARD(self); return self; @@ -146,7 +159,7 @@ static bool ractor_close_port(rb_execution_context_t *ec, rb_ractor_t *r, const static VALUE ractor_port_closed_p(rb_execution_context_t *ec, VALUE self) { - const struct ractor_port *rp = RACTOR_PORT_PTR(self); + const struct ractor_port *rp = ractor_port_ptr_check(self); rb_ractor_t *r = rp->r; bool closed; @@ -173,7 +186,7 @@ ractor_port_closed_p(rb_execution_context_t *ec, VALUE self) static VALUE ractor_port_close(rb_execution_context_t *ec, VALUE self) { - const struct ractor_port *rp = RACTOR_PORT_PTR(self); + const struct ractor_port *rp = ractor_port_ptr_check(self); rb_ractor_t *cr = rb_ec_ractor_ptr(ec); if (cr != rp->r) { @@ -560,7 +573,7 @@ ractor_monitor(rb_execution_context_t *ec, VALUE self, VALUE port) { rb_ractor_t *r = RACTOR_PTR(self); bool terminated = false; - const struct ractor_port *rp = RACTOR_PORT_PTR(port); + const struct ractor_port *rp = ractor_port_ptr_check(port); struct ractor_monitor *rm = ALLOC(struct ractor_monitor); rm->port = *rp; // copy port information @@ -592,7 +605,7 @@ static VALUE ractor_unmonitor(rb_execution_context_t *ec, VALUE self, VALUE port) { rb_ractor_t *r = RACTOR_PTR(self); - const struct ractor_port *rp = RACTOR_PORT_PTR(port); + const struct ractor_port *rp = ractor_port_ptr_check(port); RACTOR_LOCK(r); { @@ -1310,7 +1323,7 @@ ractor_selector_add(VALUE selv, VALUE rpv) } struct ractor_selector *s = RACTOR_SELECTOR_PTR(selv); - const struct ractor_port *rp = RACTOR_PORT_PTR(rpv); + const struct ractor_port *rp = ractor_port_ptr_check(rpv); if (st_lookup(s->ports, (st_data_t)rpv, NULL)) { rb_raise(rb_eArgError, "already added"); From 438674be005c543ae8e29b16dc9702fb90591a57 Mon Sep 17 00:00:00 2001 From: Nozomi Hijikata <121233810+nozomemein@users.noreply.github.com> Date: Wed, 29 Jul 2026 02:01:24 +0900 Subject: [PATCH 3/4] ZJIT: Require trivial inline target to end at leave (#18063) * ZJIT: Fix trivial inlining for optional argument entries Use SendDirect's jit_entry_idx when reading the callee bytecode for trivial inlining, so optional-argument calls inspect the same entry point that SendDirect selected. This avoids inlining default-argument initialization code when the caller actually provided the optional positional argument. * ZJIT: Require trivial inline target to end at leave Limit the trivial inliner to whole-ISEQ shapes that start at bytecode index 0 and end with the second instruction's leave. Optional argument entries can start in the middle of the ISEQ, so keep those SendDirect calls for the general inliner, which handles jit_entry_idx. This avoids folding default-argument initialization code when the caller actually supplied the optional positional argument. Co-authored-by: Takashi Kokubun (k0kubun) Co-authored-by: Alan Wu --- zjit/src/codegen_tests.rs | 20 ++++++++++++ zjit/src/hir.rs | 6 ++++ zjit/src/hir/opt_tests.rs | 68 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 22fde4a9ef21c7..d03bd2d65e52e3 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -1056,6 +1056,26 @@ fn test_send_optional_and_rest_arguments() { assert_snapshot!(assert_compiles("entry"), @"[[1, 2, []], [3, 4, []], [5, 6, [7, 8]]]"); } +#[test] +fn test_send_optional_return_default_without_argument() { + eval(" + def test(arg = nil || (return :default)) = arg + def entry = test + entry + "); + assert_snapshot!(assert_compiles("entry"), @":default"); +} + +#[test] +fn test_send_optional_return_default_with_argument() { + eval(" + def test(arg = nil || (return :default)) = arg + def entry = test(1) + entry + "); + assert_snapshot!(assert_compiles("entry"), @"1"); +} + #[test] fn test_send_rest_arguments_with_keyword_to_positional_hash() { eval(" diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index bc65eae1b206b2..21c1c701cf0897 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2750,6 +2750,12 @@ fn iseq_get_return_value(iseq: IseqPtr, captured_opnd: Option, ci_flags: if second_insn != YARVINSN_leave { return None; } + // Make sure the leave is the final instruction. Otherwise this is a + // non-trivial ISEQ that should go through the general inliner. + let iseq_size = unsafe { get_iseq_encoded_size(iseq) }; + if insn_len(first_insn as usize) + insn_len(second_insn as usize) != iseq_size { + return None; + } match first_insn { YARVINSN_getlocal_WC_0 => { // Accept only cases where only positional arguments are used by both the callee and the caller. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 9cc439834b07f0..1682fe23eca4bb 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -19347,6 +19347,74 @@ mod hir_opt_tests { "); } + #[test] + fn test_inline_method_with_omitted_optional_return_default() { + // With the optional omitted, the general inliner enters entry 0 and + // runs the default expression path. + eval(" + def callee(arg = nil || (return :default)) + arg + end + def test = callee + test + test + "); + assert_snapshot!(hir_string_with_inlining("test"), @" + fn test@:5: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile + v40:NilClass = Const Value(nil) + PushInlineFrame v18 (0x1038) + v26:StaticSymbol[:default] = Const Value(VALUE(0x1040)) + CheckInterrupts + PopInlineFrame + Return v26 + "); + } + + #[test] + fn test_inline_method_with_supplied_optional_return_default() { + // With the optional supplied, the general inliner uses the selected + // optional entry instead of entry 0, so the default-expression return + // is not inlined. + eval(" + def callee(arg = nil || (return :default)) + arg + end + def test = callee(3) + test + test + "); + assert_snapshot!(hir_string_with_inlining("test"), @" + fn test@:5: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v11:Fixnum[3] = Const Value(3) + PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile + PushInlineFrame v20 (0x1038), v11 + CheckInterrupts + PopInlineFrame + Return v11 + "); + } + #[test] fn test_inline_method_with_rescue_handler() { eval(" From 3ab7644a89c83e01263079021158f4c401c2f836 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 13 Jul 2026 15:33:03 -0700 Subject: [PATCH 4/4] Add writebarrier for Array#reverse --- array.c | 1 + 1 file changed, 1 insertion(+) diff --git a/array.c b/array.c index 4b1343b6fa51bc..ed0c8e10bc48bb 100644 --- a/array.c +++ b/array.c @@ -3387,6 +3387,7 @@ rb_ary_reverse_m(VALUE ary) const VALUE *p1 = RARRAY_CONST_PTR(ary); VALUE *p2 = (VALUE *)RARRAY_CONST_PTR(dup) + len - 1; do *p2-- = *p1++; while (--len > 0); + rb_gc_writebarrier_remember(dup); } ARY_SET_LEN(dup, RARRAY_LEN(ary)); return dup;