From ce15950f5674f571fc9fdbb1d4c2eca080c0ed2b Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Sat, 29 Aug 2026 18:39:37 +0000 Subject: [PATCH 1/8] thread_sched: keep io fds registered in epoll with EPOLLONESHOT A blocking read used to cost four epoll_ctl per wait cycle: ADD before the wait and DEL after the wake, on both ends of an exchange. The DEL ran on the timer thread, the one serial resource every io readiness event already passes through. With EPOLLONESHOT the delivery itself disarms the fd inside the kernel: the post-wake DEL becomes no syscall at all, and the next wait re-arms with one MOD on a registration that persists for the fd's lifetime. A disarm with no delivery (timeout, interrupt) MODs the event mask to zero instead of deleting. Registration stays level-triggered, so a woken reader is under no obligation to drain the fd; the generation tag already guards against events queued under an old arming, and a stale registration left by close is repaired by the existing ENOENT fallback on the next arm. Two round trips of a 64-byte socketpair exchange drop from 12 to 10 syscalls (epoll_ctl 4.00 -> 2.00) and the timer thread falls from 97% to 82% of a core. On master 22e4a75d67, sp4 (16 cores), medians: io_intensive (socketpair round trips, 2 Ractors/pair) 8 pairs 218,839 -> 432,810 rt/s (+98%) 64 pairs 276,556 -> 591,171 rt/s (+114%) punicorn (one Ractor per connection, wrk from a second box) keepalive c=64 280,678 -> 309,530 rps (+10%) 4x memcached/req 59,683 -> 67,082 rps (+12%) SSE probe beside 512 held streams 193,769 -> 205,602 rps (+6%) Workloads that wait on the timer wheel (sleep), the GVL (cpu), or connection churn are unchanged; so are Ractor creation, GC and plain threads (~50 configurations within +-2%). make btest and full test-all pass with the feature on, as does btest on a RUBY_DEBUG=1 build, and an adversarial fd close/reuse + cross-thread close + fork-with-live-io stress. Platforms: epoll only; kqueue keeps its current path (EV_DISPATCH would be the analogue). EPOLLONESHOT predates every kernel that can build ruby (Linux 2.6.2), and an epoll without it now falls back to USE_MN_THREADS=0 at compile time. Co-Authored-By: Claude Opus 5 --- thread_pthread.c | 7 ++++++- thread_sched.h | 3 +++ thread_sched_mn.c | 47 ++++++++++++++++++++++++++++++----------------- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/thread_pthread.c b/thread_pthread.c index 275a4f9b97a4de..8f6a7b11667b4b 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -92,7 +92,12 @@ static const void *const condattr_monotonic = NULL; #define USE_MN_THREADS 0 #elif HAVE_SYS_EPOLL_H #include - #define USE_MN_THREADS 1 + #ifdef EPOLLONESHOT + #define USE_MN_THREADS 1 + #else + // the scheduler arms io fds with EPOLLONESHOT (Linux 2.6.2) + #define USE_MN_THREADS 0 + #endif #elif HAVE_SYS_EVENT_H #include #define USE_MN_THREADS 1 diff --git a/thread_sched.h b/thread_sched.h index 0fe5bd951133d3..63ca7d003b6204 100644 --- a/thread_sched.h +++ b/thread_sched.h @@ -77,6 +77,9 @@ struct rb_fd_waiters { // what its waiters asked for. uint32_t armed_flags; + // epoll only: the fd is in the interest set (kept across oneshot disarms). + bool registered; + // Bumped on full disarm. Events carry the generation they were armed with, // so one queued before the fd was disarmed (and reused) is recognised. uint32_t generation; diff --git a/thread_sched_mn.c b/thread_sched_mn.c index ddf35b459b8602..038fe0b5fba1df 100644 --- a/thread_sched_mn.c +++ b/thread_sched_mn.c @@ -1228,11 +1228,14 @@ fd_event_tag(int fd, uint32_t generation) // Make the backend match `want`. Returns false if the fd cannot be registered // at all (closed, or unsupported by the backend), leaving the entry untouched. -// The fd's shard lock must be held. +// The fd's shard lock must be held. `consumed` says an epoll event for the +// current arming was just delivered, so EPOLLONESHOT has already disarmed it. static bool -fd_waiters_arm(int fd, struct rb_fd_waiters *e, uint32_t want) +fd_waiters_arm(int fd, struct rb_fd_waiters *e, uint32_t want, bool consumed) { - if (want == e->armed_flags) return true; + // After a delivery the kernel side is disarmed even when the flags agree, + // so a consumed call must fall through to re-arm. + if (want == e->armed_flags && !consumed) return true; #if HAVE_SYS_EVENT_H struct kevent ke[2]; @@ -1268,15 +1271,22 @@ fd_waiters_arm(int fd, struct rb_fd_waiters *e, uint32_t want) } #elif HAVE_SYS_EPOLL_H if (want == 0) { - if (epoll_ctl(timer_th.event_fd, EPOLL_CTL_DEL, fd, NULL) == -1) { - switch (errno) { - case EBADF: - case ENOENT: - // the fd is already closed or gone from the set - break; - default: - perror("epoll_ctl"); - rb_bug("fd_waiters_arm/epoll_ctl del failed (fd:%d errno:%d)", fd, errno); + // A delivered oneshot event has already disarmed the fd; otherwise + // disarm by MOD to no events. Either way the registration stays, so + // the next wait is one MOD instead of DEL + ADD. + if (!consumed && e->registered) { + struct epoll_event off = { .events = 0, .data = { .u64 = 0 } }; + if (epoll_ctl(timer_th.event_fd, EPOLL_CTL_MOD, fd, &off) == -1) { + switch (errno) { + case EBADF: + case ENOENT: + // the fd is already closed or gone from the set + e->registered = false; + break; + default: + perror("epoll_ctl"); + rb_bug("fd_waiters_arm/epoll_ctl disarm failed (fd:%d errno:%d)", fd, errno); + } } } // Anything epoll_wait already queued for the old arming is stale now. @@ -1285,7 +1295,7 @@ fd_waiters_arm(int fd, struct rb_fd_waiters *e, uint32_t want) return true; } - uint32_t epoll_events = 0; + uint32_t epoll_events = EPOLLONESHOT; if (want & thread_sched_waiting_io_read) epoll_events |= EPOLLIN; if (want & thread_sched_waiting_io_write) epoll_events |= EPOLLOUT; @@ -1294,7 +1304,7 @@ fd_waiters_arm(int fd, struct rb_fd_waiters *e, uint32_t want) .data = { .u64 = fd_event_tag(fd, e->generation) }, }; - int op = e->armed_flags ? EPOLL_CTL_MOD : EPOLL_CTL_ADD; + int op = e->registered ? EPOLL_CTL_MOD : EPOLL_CTL_ADD; if (epoll_ctl(timer_th.event_fd, op, fd, &event) == -1) { switch (errno) { @@ -1304,6 +1314,7 @@ fd_waiters_arm(int fd, struct rb_fd_waiters *e, uint32_t want) epoll_ctl(timer_th.event_fd, EPOLL_CTL_ADD, fd, &event) == 0) { break; } + e->registered = false; return false; case EEXIST: // Likewise in the other direction. @@ -1315,12 +1326,14 @@ fd_waiters_arm(int fd, struct rb_fd_waiters *e, uint32_t want) case EBADF: case EPERM: // closed, or the fd does not support epoll + e->registered = false; return false; default: perror("epoll_ctl"); rb_bug("fd_waiters_arm/epoll_ctl failed (fd:%d op:%d errno:%d)", fd, op, errno); } } + e->registered = true; #else # error "neither kqueue nor epoll" #endif @@ -1493,7 +1506,7 @@ timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting // Arm the union of what this fd's waiters want, so a second waiter // on the same fd extends the arming instead of colliding with it. - if (!fd_waiters_arm(fd, e, fd_waiters_union(e) | (uint32_t)(flags & FD_WAIT_IO_MASK))) { + if (!fd_waiters_arm(fd, e, fd_waiters_union(e) | (uint32_t)(flags & FD_WAIT_IO_MASK), false)) { fd_shard_unlock(fd); return timer_thread_unavailable; } @@ -1573,7 +1586,7 @@ timer_thread_unregister_waiting(rb_thread_t *th, int fd, enum thread_sched_waiti struct rb_fd_waiters *e = fd_waiters_lookup(fd, false); if (e) { - fd_waiters_arm(fd, e, fd_waiters_union(e)); + fd_waiters_arm(fd, e, fd_waiters_union(e), false); } } @@ -1689,7 +1702,7 @@ timer_thread_wake_fd_waiters(int fd, uint32_t generation, uint32_t wake_flags, i // Re-arm for whoever is still waiting on this fd (nothing, if // they all just woke up). - fd_waiters_arm(fd, e, fd_waiters_union(e)); + fd_waiters_arm(fd, e, fd_waiters_union(e), true); } } fd_shard_unlock(fd); From f20155d53c05bd0b4dd47a9f4a3177fb3a34a62f Mon Sep 17 00:00:00 2001 From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:47:59 +0100 Subject: [PATCH 2/8] marshal.c: Make partial objects table conditional Only allocate and use the partial objects table when a load proc is active or loaded objects must be frozen. Co-authored-by: Codex --- benchmark/marshal_load_partial_objects.yml | 74 ++++++++++++++++++++++ marshal.c | 25 +++++--- test/ruby/test_marshal.rb | 8 +++ 3 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 benchmark/marshal_load_partial_objects.yml diff --git a/benchmark/marshal_load_partial_objects.yml b/benchmark/marshal_load_partial_objects.yml new file mode 100644 index 00000000000000..f13c338db20aed --- /dev/null +++ b/benchmark/marshal_load_partial_objects.yml @@ -0,0 +1,74 @@ +prelude: | + MarshalLoadPartialObjectsObject = Class.new do + def initialize(a, b, c, d) + @a = a + @b = b + @c = c + @d = d + end + end + + MarshalLoadPartialObjectsStruct = Struct.new(:a, :b, :c, :d) + MarshalLoadPartialObjectsData = Data.define(:a, :b, :c, :d) + + integer_array_10 = 10.times.to_a + integer_array_1000 = 1000.times.to_a + unique_strings = 1000.times.map { |i| "string #{i}" } + shared_string = +"shared string" + shared_strings = Array.new(1000, shared_string) + integer_hash = 1000.times.to_h { |i| [i, i] } + symbol_string_hash = 1000.times.to_h { |i| ["key#{i}".to_sym, "value #{i}"] } + objects = 1000.times.map { |i| MarshalLoadPartialObjectsObject.new(i, i + 1, i + 2, i + 3) } + structs = 1000.times.map { |i| MarshalLoadPartialObjectsStruct.new(i, i + 1, i + 2, i + 3) } + data_objects = 1000.times.map { |i| MarshalLoadPartialObjectsData.new(i, i + 1, i + 2, i + 3) } + + integer_array_10_dump = Marshal.dump(integer_array_10) + integer_array_1000_dump = Marshal.dump(integer_array_1000) + unique_strings_dump = Marshal.dump(unique_strings) + shared_strings_dump = Marshal.dump(shared_strings) + integer_hash_dump = Marshal.dump(integer_hash) + symbol_string_hash_dump = Marshal.dump(symbol_string_hash) + objects_dump = Marshal.dump(objects) + structs_dump = Marshal.dump(structs) + data_objects_dump = Marshal.dump(data_objects) + + load_proc = ->(object) { object } + +benchmark: + marshal_load_integer_array_10: 'Marshal.load(integer_array_10_dump)' + marshal_load_integer_array_10_with_proc: 'Marshal.load(integer_array_10_dump, load_proc)' + marshal_load_integer_array_10_freeze: 'Marshal.load(integer_array_10_dump, freeze: true)' + + marshal_load_integer_array_1000: 'Marshal.load(integer_array_1000_dump)' + marshal_load_integer_array_1000_with_proc: 'Marshal.load(integer_array_1000_dump, load_proc)' + marshal_load_integer_array_1000_freeze: 'Marshal.load(integer_array_1000_dump, freeze: true)' + + marshal_load_unique_strings: 'Marshal.load(unique_strings_dump)' + marshal_load_unique_strings_with_proc: 'Marshal.load(unique_strings_dump, load_proc)' + marshal_load_unique_strings_freeze: 'Marshal.load(unique_strings_dump, freeze: true)' + + marshal_load_shared_strings: 'Marshal.load(shared_strings_dump)' + marshal_load_shared_strings_with_proc: 'Marshal.load(shared_strings_dump, load_proc)' + marshal_load_shared_strings_freeze: 'Marshal.load(shared_strings_dump, freeze: true)' + + marshal_load_integer_hash: 'Marshal.load(integer_hash_dump)' + marshal_load_integer_hash_with_proc: 'Marshal.load(integer_hash_dump, load_proc)' + marshal_load_integer_hash_freeze: 'Marshal.load(integer_hash_dump, freeze: true)' + + marshal_load_symbol_string_hash: 'Marshal.load(symbol_string_hash_dump)' + marshal_load_symbol_string_hash_with_proc: 'Marshal.load(symbol_string_hash_dump, load_proc)' + marshal_load_symbol_string_hash_freeze: 'Marshal.load(symbol_string_hash_dump, freeze: true)' + + marshal_load_objects: 'Marshal.load(objects_dump)' + marshal_load_objects_with_proc: 'Marshal.load(objects_dump, load_proc)' + marshal_load_objects_freeze: 'Marshal.load(objects_dump, freeze: true)' + + marshal_load_structs: 'Marshal.load(structs_dump)' + marshal_load_structs_with_proc: 'Marshal.load(structs_dump, load_proc)' + marshal_load_structs_freeze: 'Marshal.load(structs_dump, freeze: true)' + + marshal_load_data_objects: 'Marshal.load(data_objects_dump)' + marshal_load_data_objects_with_proc: 'Marshal.load(data_objects_dump, load_proc)' + marshal_load_data_objects_freeze: 'Marshal.load(data_objects_dump, freeze: true)' + +loop_count: 1000 diff --git a/marshal.c b/marshal.c index 3ad30efa7e8f2c..a9fd5aeb918d47 100644 --- a/marshal.c +++ b/marshal.c @@ -1275,7 +1275,7 @@ mark_load_arg(void *ptr) return; rb_mark_tbl(p->symbols); rb_mark_tbl(p->data); - rb_mark_tbl(p->partial_objects); + if (p->partial_objects) rb_mark_tbl(p->partial_objects); rb_mark_hash(p->compat_tbl); } @@ -1658,7 +1658,9 @@ r_entry0(VALUE v, st_index_t num, struct load_arg *arg) st_lookup(arg->compat_tbl, v, &real_obj); } st_insert(arg->data, num, real_obj); - st_insert(arg->partial_objects, (st_data_t)real_obj, Qtrue); + if (arg->partial_objects) { + st_insert(arg->partial_objects, (st_data_t)real_obj, Qtrue); + } return v; } @@ -1693,9 +1695,11 @@ r_leave(VALUE v, struct load_arg *arg, bool partial) { v = r_fixup_compat(v, arg); if (!partial) { - st_data_t data; - st_data_t key = (st_data_t)v; - st_delete(arg->partial_objects, &key, &data); + if (arg->partial_objects) { + st_data_t data; + st_data_t key = (st_data_t)v; + st_delete(arg->partial_objects, &key, &data); + } if (arg->freeze) { if (RB_TYPE_P(v, T_MODULE) || RB_TYPE_P(v, T_CLASS)) { // noop @@ -1890,7 +1894,8 @@ r_object_for(struct load_arg *arg, bool partial, int *ivp, VALUE klass, VALUE ex rb_raise(rb_eArgError, "dump format error (unlinked)"); } v = (VALUE)link; - if (!st_lookup(arg->partial_objects, (st_data_t)v, &link)) { + if (arg->partial_objects && + !st_lookup(arg->partial_objects, (st_data_t)v, &link)) { if (arg->freeze && RB_TYPE_P(v, T_STRING)) { v = rb_str_to_interned_str(v); } @@ -2382,8 +2387,10 @@ clear_load_arg(struct load_arg *arg) arg->symbols = 0; st_free_table(arg->data); arg->data = 0; - st_free_table(arg->partial_objects); - arg->partial_objects = 0; + if (arg->partial_objects) { + st_free_table(arg->partial_objects); + arg->partial_objects = 0; + } if (arg->compat_tbl) { st_free_table(arg->compat_tbl); arg->compat_tbl = 0; @@ -2413,7 +2420,7 @@ rb_marshal_load_with_proc(VALUE port, VALUE proc, bool freeze) arg->offset = 0; arg->symbols = st_init_numtable(); arg->data = rb_init_identtable(); - arg->partial_objects = rb_init_identtable(); + arg->partial_objects = (RTEST(proc) || freeze) ? rb_init_identtable() : NULL; arg->compat_tbl = 0; arg->proc = 0; arg->readable = 0; diff --git a/test/ruby/test_marshal.rb b/test/ruby/test_marshal.rb index 84c00a6502c71b..b7e40cb2d3600d 100644 --- a/test/ruby/test_marshal.rb +++ b/test/ruby/test_marshal.rb @@ -769,6 +769,14 @@ def test_marshal_proc_freeze assert_equal object, Marshal.load(Marshal.dump(object), :freeze.to_proc) end + def test_marshal_false_proc + object = [] + object << object + + loaded = Marshal.load(Marshal.dump(object), false) + assert_same loaded, loaded.first + end + def test_marshal_load_extended_class_crash assert_separately([], "#{<<-"begin;"}\n#{<<-"end;"}") begin; From 0f20fd9f88ba2fff0712ccdd7bfd8bb3e2eefea7 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 27 Aug 2026 10:55:43 -0400 Subject: [PATCH 3/8] ZJIT: Support generating Const::CBool in codegen --- zjit/src/codegen.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 6e860241be08a9..3b30e1bb2563de 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -633,6 +633,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio assert_eq!(SHAPE_ID_NUM_BITS, 32); gen_const_uint32(val.0) } + &Insn::Const { val: Const::CBool(val) } => Opnd::UImm(val.into()), Insn::Const { .. } => panic!("Unexpected Const in gen_insn: {insn}"), Insn::NewArray { elements, state } => gen_new_array(jit, asm, function, opnds!(elements), &function.frame_state(*state)), Insn::NewHash { elements, state } => { From e5cd8f00ff420fbc73132db5b807b0286ca484bd Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 27 Aug 2026 10:23:21 -0400 Subject: [PATCH 4/8] ZJIT: Specialize FalseClass#& It just returns `false` no matter the other argument. --- zjit/src/cruby_methods.rs | 7 +++++++ zjit/src/hir/opt_tests.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 43bda091ae2996..fc01a6b35931db 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -277,6 +277,7 @@ pub fn init() -> Annotations { annotate!(rb_cFloat, "nan?", types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cFloat, "finite?", types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cFloat, "infinite?", types::Fixnum.union(types::NilClass), no_gc, leaf, elidable); + annotate!(rb_cFalseClass, "&", inline_falseclass_and); let thread_singleton = unsafe { rb_singleton_class(rb_cThread) }; annotate!(thread_singleton, "current", inline_thread_current, types::BasicObject, no_gc, leaf); @@ -319,6 +320,12 @@ fn inline_string_to_s(fun: &mut hir::Function, block: hir::BlockId, recv: hir::I None } +fn inline_falseclass_and(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { + // FalseClass#& just returns Qfalse and ignores its argument. + let &[_] = args else { return None; }; + Some(fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(Qfalse) })) +} + fn inline_thread_current(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { let &[] = args else { return None; }; let ec = fun.push_insn(block, hir::Insn::LoadEC); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 0b3665c3182fa5..666dd435914adb 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -22006,4 +22006,37 @@ mod hir_opt_tests { Return v49 "); } + + #[test] + fn test_optimize_falseclass_and_to_false() { + eval(r#" + def test(cond) + cond & "hello" + end + + test(false) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :cond@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :cond@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + v15:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v16:StringExact = StringCopy v15 + PatchPoint MethodRedefined(FalseClass@0x1010, &@0x1018, cme:0x1020) + v27:FalseClass = GuardType v10, FalseClass recompile + v28:FalseClass = Const Value(false) + CheckInterrupts + Return v28 + "); + } } From b6071e1e6a047ee9057b267e445cc81cdce6098c Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 27 Aug 2026 10:25:14 -0400 Subject: [PATCH 5/8] ZJIT: Specialize TrueClass#& It just does `RBOOL(RTEST(other))`. --- zjit/src/cruby_methods.rs | 8 ++++++++ zjit/src/hir/opt_tests.rs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index fc01a6b35931db..7becc9eb5536c4 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -278,6 +278,7 @@ pub fn init() -> Annotations { annotate!(rb_cFloat, "finite?", types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cFloat, "infinite?", types::Fixnum.union(types::NilClass), no_gc, leaf, elidable); annotate!(rb_cFalseClass, "&", inline_falseclass_and); + annotate!(rb_cTrueClass, "&", inline_trueclass_and); let thread_singleton = unsafe { rb_singleton_class(rb_cThread) }; annotate!(thread_singleton, "current", inline_thread_current, types::BasicObject, no_gc, leaf); @@ -326,6 +327,13 @@ fn inline_falseclass_and(fun: &mut hir::Function, block: hir::BlockId, _recv: hi Some(fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(Qfalse) })) } +fn inline_trueclass_and(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { + // TrueClass#& does RBOOL(RTEST(arg)) + let &[val] = args else { return None; }; + let test = fun.push_insn(block, hir::Insn::Test { val }); + Some(fun.push_insn(block, hir::Insn::BoxBool { val: test })) +} + fn inline_thread_current(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { let &[] = args else { return None; }; let ec = fun.push_insn(block, hir::Insn::LoadEC); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 666dd435914adb..dc25a260336a21 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -22039,4 +22039,38 @@ mod hir_opt_tests { Return v28 "); } + + #[test] + fn test_optimize_trueclass_and_to_test() { + eval(r#" + def test(l, r) + l & r + end + + test(true, "hello") + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :l@0x1000 + v4:BasicObject = LoadField v2, :r@0x1001 + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :l@1 + v9:BasicObject = LoadArg :r@2 + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): + PatchPoint MethodRedefined(TrueClass@0x1008, &@0x1010, cme:0x1018) + v28:TrueClass = GuardType v12, TrueClass recompile + v29:CBool = Test v13 + v30:BoolExact = BoxBool v29 + CheckInterrupts + Return v30 + "); + } } From 6ce41703c55099563731e4ab19af1b8d539fae17 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 27 Aug 2026 10:32:40 -0400 Subject: [PATCH 6/8] ZJIT: Add line numbers to Perfetto traces This helps figure out what part of long functions is causing the exit or fallback or whatever is being traced. --- zjit/src/state.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zjit/src/state.rs b/zjit/src/state.rs index 183b3c206ee813..9889997d1e4838 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -449,7 +449,7 @@ pub extern "C" fn rb_zjit_assert_compiles(_ec: EcPtr, _self: VALUE) -> VALUE { } /// Resolve a profile frame VALUE to a human-readable "label (path)" string. -fn resolve_frame_label(frame: VALUE) -> String { +fn resolve_frame_label(frame: VALUE, line_number: i32) -> String { unsafe { let label_str = ruby_str_to_rust_string_result(rb_profile_frame_full_label(frame)).unwrap_or("".into()); @@ -457,7 +457,7 @@ fn resolve_frame_label(frame: VALUE) -> String { let path = if path.nil_p() { rb_profile_frame_path(frame) } else { path }; let path_str = ruby_str_to_rust_string_result(path).unwrap_or("".into()); - format!("{label_str} ({path_str})") + format!("{label_str} ({path_str}:{line_number})") } } @@ -574,6 +574,6 @@ fn capture_ruby_frames() -> Vec { // Resolve each frame to a human-readable string (top frame first) (0..stack_length as usize) - .map(|i| resolve_frame_label(frames_buffer[i])) + .map(|i| resolve_frame_label(frames_buffer[i], lines_buffer[i])) .collect() } From 4068011d65fca13c71dbb57243053d627dcf5752 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 27 Aug 2026 10:38:33 -0400 Subject: [PATCH 7/8] ZJIT: Fold BoxBool(Test(cond:BoolExact)) to cond This appears when doing e.g. `l & (a == b)` (as in the included new test) where `TrueClass#&` does `RTEST(RBOOL(rhs))` and then the right hand side is already known to be returning a `BoolExact` because it's from `FixnumEq`. --- zjit/src/hir.rs | 14 ++++++++++++++ zjit/src/hir/opt_tests.rs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 9a6461b9301d93..d4f9e6140e9496 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -6714,6 +6714,20 @@ impl Function { insn_id } } + &Insn::BoxBool { val: bool_val } => { + if let &Insn::Test { val: test_val } = self.resolve(bool_val).insn(self) { + // If the thing being Test'd is already a BoolExact + // (TrueClass|FalseClass), then we don't need to Test+BoxBool and can + // just return the test_val. + if self.is_a(test_val, types::BoolExact) { + self.make_equal_to(insn_id, test_val); + continue; + } + insn_id + } else { + insn_id + } + } &Insn::CondBranch { val, ref if_true, .. } if self.is_a(val, Type::from_cbool(true)) => { self.new_insn(Insn::Jump(if_true.clone())) } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index dc25a260336a21..22d722ada4ee9e 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -22073,4 +22073,42 @@ mod hir_opt_tests { Return v30 "); } + + #[test] + fn test_remove_box_of_test_of_bool_exact() { + eval(r#" + def test(l, a, b) + l & (a == b) + end + + test(true, 3, 4) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :l@0x1000 + v4:BasicObject = LoadField v2, :a@0x1001 + v5:BasicObject = LoadField v2, :b@0x1002 + Jump bb3(v1, v3, v4, v5) + bb2(): + EntryPoint JIT(0) + v8:BasicObject = LoadArg :self@0 + v9:BasicObject = LoadArg :l@1 + v10:BasicObject = LoadArg :a@2 + v11:BasicObject = LoadArg :b@3 + Jump bb3(v8, v9, v10, v11) + bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:BasicObject): + PatchPoint MethodRedefined(Integer@0x1008, ==@0x1010, cme:0x1018) + v35:Fixnum = GuardType v15, Fixnum recompile + v36:Fixnum = GuardType v16, Fixnum + v37:BoolExact = FixnumEq v35, v36 + PatchPoint MethodRedefined(TrueClass@0x1040, &@0x1048, cme:0x1050) + v40:TrueClass = GuardType v14, TrueClass recompile + CheckInterrupts + Return v37 + "); + } } From a889da909ca2ae7aa94be69a850aac2e1ef6bdbe Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 27 Aug 2026 14:36:10 -0400 Subject: [PATCH 8/8] ZJIT: Remove WriteBarrier for immediates It doesn't end up generating any code (we catch this in codegen too) but we might as well not have the instruction lying around in our IR. --- zjit/src/hir.rs | 4 ++++ zjit/src/hir/opt_tests.rs | 30 ------------------------------ 2 files changed, 4 insertions(+), 30 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index d4f9e6140e9496..efe30cf05f66ff 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -6447,6 +6447,10 @@ impl Function { _ => insn_id, } } + &Insn::WriteBarrier { val, .. } if self.is_a(val, types::Immediate) => { + // The write barrier does nothing for immediates. + continue; + } &Insn::ArrayLength { array } => { match self.type_of(array).ruby_object() { Some(array_obj) if array_obj.is_frozen() => { diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 22d722ada4ee9e..5112f422c1467a 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6172,7 +6172,6 @@ mod hir_opt_tests { v63:CShape = LoadField v45, :shape_id@0x1088 v64:CShape[0x1089] = GuardBitEquals v63, CShape(0x1089) recompile StoreField v45, :@x@0x108a, v15 - WriteBarrier v45, v15 v67:CShape[0x108b] = Const CShape(0x108b) StoreField v45, :shape_id@0x1088, v67 CheckInterrupts @@ -7844,7 +7843,6 @@ mod hir_opt_tests { v15:CShape = LoadField v14, :shape_id@0x1000 v16:CShape[0x1001] = GuardBitEquals v15, CShape(0x1001) recompile StoreField v14, :@foo@0x1002, v10 - WriteBarrier v14, v10 CheckInterrupts Return v10 "); @@ -7916,7 +7914,6 @@ mod hir_opt_tests { v15:CShape = LoadField v14, :shape_id@0x1000 v16:CShape[0x1001] = GuardBitEquals v15, CShape(0x1001) recompile StoreField v14, :@foo@0x1002, v10 - WriteBarrier v14, v10 v19:CShape[0x1003] = Const CShape(0x1003) StoreField v14, :shape_id@0x1000, v19 CheckInterrupts @@ -7985,13 +7982,11 @@ mod hir_opt_tests { v14:CShape = LoadField v13, :shape_id@0x1000 v15:CShape[0x1001] = GuardBitEquals v14, CShape(0x1001) recompile StoreField v13, :@foo@0x1002, v10 - WriteBarrier v13, v10 v18:CShape[0x1003] = Const CShape(0x1003) StoreField v13, :shape_id@0x1000, v18 v23:Fixnum[2] = Const Value(2) PatchPoint SingleRactorMode StoreField v13, :@bar@0x1004, v23 - WriteBarrier v13, v23 v32:CShape[0x1005] = Const CShape(0x1005) StoreField v13, :shape_id@0x1000, v32 CheckInterrupts @@ -8064,14 +8059,12 @@ mod hir_opt_tests { CondBranch v17, bb5(), bb6() bb5(): StoreField v14, :@a@0x1002, v10 - WriteBarrier v14, v10 v21:CShape[0x1003] = Const CShape(0x1003) StoreField v14, :shape_id@0x1000, v21 Jump bb4() bb6(): v24:CShape[0x1004] = GuardBitEquals v15, CShape(0x1004) recompile StoreField v14, :@a@0x1005, v10 - WriteBarrier v14, v10 Jump bb4() bb4(): CheckInterrupts @@ -10368,12 +10361,10 @@ mod hir_opt_tests { CondBranch v17, bb5(), bb6() bb5(): StoreField v6, :@foo@0x1002, v10 - WriteBarrier v6, v10 Jump bb4() bb6(): v22:CShape[0x1003] = GuardBitEquals v15, CShape(0x1003) recompile StoreField v6, :@foo@0x1004, v10 - WriteBarrier v6, v10 Jump bb4() bb4(): CheckInterrupts @@ -11677,7 +11668,6 @@ mod hir_opt_tests { v30:CShape = LoadField v28, :shape_id@0x1040 v31:CShape[0x1041] = GuardBitEquals v30, CShape(0x1041) StoreField v28, :@foo@0x1042, v17 - WriteBarrier v28, v17 v34:CShape[0x1043] = Const CShape(0x1043) StoreField v28, :shape_id@0x1040, v34 CheckInterrupts @@ -11716,7 +11706,6 @@ mod hir_opt_tests { v30:CShape = LoadField v28, :shape_id@0x1040 v31:CShape[0x1041] = GuardBitEquals v30, CShape(0x1041) StoreField v28, :@foo@0x1042, v17 - WriteBarrier v28, v17 v34:CShape[0x1043] = Const CShape(0x1043) StoreField v28, :shape_id@0x1040, v34 CheckInterrupts @@ -12547,7 +12536,6 @@ mod hir_opt_tests { v39:CInt64 = ArrayLength v33 v40:CInt64[1] = GuardLess v46, v39 ArrayAset v33, v40, v19 - WriteBarrier v33, v19 CheckInterrupts Return v19 "); @@ -18783,12 +18771,10 @@ mod hir_opt_tests { v20:CShape = LoadField v19, :shape_id@0x1000 v21:CShape[0x1001] = GuardBitEquals v20, CShape(0x1001) recompile StoreField v19, :@a@0x1002, v13 - WriteBarrier v19, v13 v24:CShape[0x1003] = Const CShape(0x1003) StoreField v19, :shape_id@0x1000, v24 PatchPoint NoEPEscape(initialize) PatchPoint SingleRactorMode - WriteBarrier v19, v13 CheckInterrupts Return v13 "); @@ -18828,7 +18814,6 @@ mod hir_opt_tests { v23:CShape = LoadField v22, :shape_id@0x1000 v24:CShape[0x1001] = GuardBitEquals v23, CShape(0x1001) recompile StoreField v22, :@a@0x1002, v16 - WriteBarrier v22, v16 v27:CShape[0x1003] = Const CShape(0x1003) StoreField v22, :shape_id@0x1000, v27 v32:Fixnum[5] = Const Value(5) @@ -18836,7 +18821,6 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) v65:Fixnum[6] = Const Value(6) PatchPoint SingleRactorMode - WriteBarrier v22, v16 CheckInterrupts Return v16 "); @@ -18874,13 +18858,10 @@ mod hir_opt_tests { v20:CShape = LoadField v19, :shape_id@0x1000 v21:CShape[0x1001] = GuardBitEquals v20, CShape(0x1001) recompile StoreField v19, :@a@0x1002, v13 - WriteBarrier v19, v13 v24:CShape[0x1003] = Const CShape(0x1003) StoreField v19, :shape_id@0x1000, v24 PatchPoint NoEPEscape(initialize) PatchPoint SingleRactorMode - WriteBarrier v19, v13 - WriteBarrier v19, v13 CheckInterrupts Return v13 "); @@ -19369,7 +19350,6 @@ mod hir_opt_tests { v68:CShape = LoadField v18, :shape_id@0x1038 v69:CShape[0x103b] = GuardBitEquals v68, CShape(0x103b) recompile StoreField v18, :@levar@0x103a, v19 - WriteBarrier v18, v19 v72:CShape[0x1039] = Const CShape(0x1039) StoreField v18, :shape_id@0x1038, v72 Jump bb5(v18) @@ -20160,7 +20140,6 @@ mod hir_opt_tests { CondBranch v23, bb5(), bb6() bb5(): StoreField v11, :@a@0x1003, v17 - WriteBarrier v11, v17 Jump bb4() bb6(): v28:CShape[0x1004] = Const CShape(0x1004) @@ -20168,7 +20147,6 @@ mod hir_opt_tests { CondBranch v29, bb7(), bb8() bb7(): StoreField v11, :@a@0x1003, v17 - WriteBarrier v11, v17 v35:CShape[0x1002] = Const CShape(0x1002) StoreField v11, :shape_id@0x1001, v35 Jump bb4() @@ -20188,7 +20166,6 @@ mod hir_opt_tests { CondBranch v57, bb10(), bb11() bb10(): StoreField v11, :@a@0x1003, v73 - WriteBarrier v11, v73 Jump bb9() bb11(): SetIvar v11, :@a, v73 @@ -20219,7 +20196,6 @@ mod hir_opt_tests { CondBranch v23, bb5(), bb6() bb5(): StoreField v11, :@a@0x1003, v17 - WriteBarrier v11, v17 Jump bb4() bb6(): v28:CShape[0x1004] = Const CShape(0x1004) @@ -20227,7 +20203,6 @@ mod hir_opt_tests { CondBranch v29, bb7(), bb8() bb7(): StoreField v11, :@a@0x1003, v17 - WriteBarrier v11, v17 v35:CShape[0x1002] = Const CShape(0x1002) StoreField v11, :shape_id@0x1001, v35 Jump bb4() @@ -21693,13 +21668,11 @@ mod hir_opt_tests { v117:CShape = LoadField v85, :shape_id@0x1088 v118:CShape[0x1089] = GuardBitEquals v117, CShape(0x1089) recompile StoreField v85, :@x@0x108a, v15 - WriteBarrier v85, v15 v121:CShape[0x108b] = Const CShape(0x108b) StoreField v85, :shape_id@0x1088, v121 PatchPoint NoEPEscape(initialize) PatchPoint SingleRactorMode StoreField v85, :@y@0x108c, v17 - WriteBarrier v85, v17 v136:CShape[0x108d] = Const CShape(0x108d) StoreField v85, :shape_id@0x1088, v136 CheckInterrupts @@ -21718,13 +21691,11 @@ mod hir_opt_tests { v157:CShape = LoadField v95, :shape_id@0x1088 v158:CShape[0x1089] = GuardBitEquals v157, CShape(0x1089) recompile StoreField v95, :@x@0x108a, v47 - WriteBarrier v95, v47 v161:CShape[0x108b] = Const CShape(0x108b) StoreField v95, :shape_id@0x1088, v161 PatchPoint NoEPEscape(initialize) PatchPoint SingleRactorMode StoreField v95, :@y@0x108c, v49 - WriteBarrier v95, v49 v176:CShape[0x108d] = Const CShape(0x108d) StoreField v95, :shape_id@0x1088, v176 CheckInterrupts @@ -21896,7 +21867,6 @@ mod hir_opt_tests { v71:Fixnum = GuardType v15, Fixnum recompile v72:Fixnum = FixnumAdd v71, v17 StoreField v14, :@hclk@0x1003, v72 - WriteBarrier v14, v72 PatchPoint SingleRactorMode v37:BasicObject = LoadField v14, :@hclk_target@0x1040 PatchPoint MethodRedefined(Integer@0x1008, <=@0x1041, cme:0x1048)