From d7f2a1ec9a1ae569bf79f06ad44e0f76292c9b06 Mon Sep 17 00:00:00 2001 From: Aiden Fox Ivey Date: Thu, 9 Oct 2025 12:42:09 -0400 Subject: [PATCH 01/14] ZJIT: Profile opt_aref (#14778) * ZJIT: Profile opt_aref * ZJIT: Add test for opt_aref * ZJIT: Move test and add hash opt test * ZJIT: Update zjit bindgen * ZJIT: Add inspect calls to opt_aref tests --- insns.def | 1 + zjit/src/cruby_bindings.inc.rs | 7 ++-- zjit/src/hir.rs | 58 ++++++++++++++++++++++++++++++++++ zjit/src/profile.rs | 1 + 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/insns.def b/insns.def index 239fe85aa51439..eef0d3f5dc1124 100644 --- a/insns.def +++ b/insns.def @@ -1519,6 +1519,7 @@ opt_aref * default_proc. This is a method call. So opt_aref is * (surprisingly) not leaf. */ // attr bool leaf = false; /* has rb_funcall() */ /* calls #yield */ +// attr bool zjit_profile = true; { val = vm_opt_aref(recv, obj); diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 2d8a8eb11e7036..8eac87c965dbea 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -696,9 +696,10 @@ pub const YARVINSN_zjit_opt_gt: ruby_vminsn_type = 231; pub const YARVINSN_zjit_opt_ge: ruby_vminsn_type = 232; pub const YARVINSN_zjit_opt_and: ruby_vminsn_type = 233; pub const YARVINSN_zjit_opt_or: ruby_vminsn_type = 234; -pub const YARVINSN_zjit_opt_empty_p: ruby_vminsn_type = 235; -pub const YARVINSN_zjit_opt_not: ruby_vminsn_type = 236; -pub const VM_INSTRUCTION_SIZE: ruby_vminsn_type = 237; +pub const YARVINSN_zjit_opt_aref: ruby_vminsn_type = 235; +pub const YARVINSN_zjit_opt_empty_p: ruby_vminsn_type = 236; +pub const YARVINSN_zjit_opt_not: ruby_vminsn_type = 237; +pub const VM_INSTRUCTION_SIZE: ruby_vminsn_type = 238; pub type ruby_vminsn_type = u32; pub type rb_iseq_callback = ::std::option::Option< unsafe extern "C" fn(arg1: *const rb_iseq_t, arg2: *mut ::std::os::raw::c_void), diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 26145f46226fbb..248c2af7abd5c8 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -9069,6 +9069,64 @@ mod opt_tests { "); } + #[test] + fn test_opt_aref_array() { + eval(" + arr = [1,2,3] + def test(arr) = arr[0] + test(arr) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v13:Fixnum[0] = Const Value(0) + PatchPoint MethodRedefined(Array@0x1000, []@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(Array@0x1000) + v26:ArrayExact = GuardType v9, ArrayExact + v27:BasicObject = CCallVariadic []@0x1038, v26, v13 + CheckInterrupts + Return v27 + "); + assert_snapshot!(inspect("test [1,2,3]"), @"1"); + } + + #[test] + fn test_opt_aref_hash() { + eval(" + arr = {0 => 4} + def test(arr) = arr[0] + test(arr) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v13:Fixnum[0] = Const Value(0) + PatchPoint MethodRedefined(Hash@0x1000, []@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(Hash@0x1000) + v26:HashExact = GuardType v9, HashExact + v27:BasicObject = CallCFunc []@0x1038, v26, v13 + CheckInterrupts + Return v27 + "); + assert_snapshot!(inspect("test({0 => 4})"), @"4"); + } + #[test] fn test_eliminate_new_range() { eval(" diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index 9588a541827f35..67f2fdc7403822 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -73,6 +73,7 @@ fn profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) { YARVINSN_opt_and => profile_operands(profiler, profile, 2), YARVINSN_opt_or => profile_operands(profiler, profile, 2), YARVINSN_opt_empty_p => profile_operands(profiler, profile, 1), + YARVINSN_opt_aref => profile_operands(profiler, profile, 2), YARVINSN_opt_not => profile_operands(profiler, profile, 1), YARVINSN_getinstancevariable => profile_self(profiler, profile), YARVINSN_objtostring => profile_operands(profiler, profile, 1), From 09e5c5eed1ee9a58ffa37ecdda999a6ffaea6eb4 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Oct 2025 19:06:49 +0200 Subject: [PATCH 02/14] ZJIT: Name enum for bindgen (#14802) Relying on having the same compiler version and behavior across platforms is brittle, as Kokubun points out. Instead, name the enum so we don't have to rely on gensym stability. Fix https://github.com/Shopify/ruby/issues/787 --- zjit.c | 2 +- zjit/bindgen/src/main.rs | 2 +- zjit/src/cruby_bindings.inc.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zjit.c b/zjit.c index d877c0bacbd507..e17abc1b37ff29 100644 --- a/zjit.c +++ b/zjit.c @@ -235,7 +235,7 @@ rb_zjit_print_exception(void) rb_warn("Ruby error: %"PRIsVALUE"", rb_funcall(exception, rb_intern("full_message"), 0)); } -enum { +enum zjit_exported_constants { RB_INVALID_SHAPE_ID = INVALID_SHAPE_ID, }; diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index e1d19f9442c62b..64b235b838baff 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -290,7 +290,7 @@ fn main() { .allowlist_function("rb_zjit_insn_leaf") .allowlist_type("robject_offsets") .allowlist_type("rstring_offsets") - .allowlist_var("RB_INVALID_SHAPE_ID") + .allowlist_type("zjit_exported_constants") .allowlist_function("rb_assert_holding_vm_lock") .allowlist_function("rb_jit_shape_too_complex_p") .allowlist_function("rb_jit_multi_ractor_p") diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 8eac87c965dbea..ab442841ff267a 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -723,8 +723,8 @@ pub const DEFINED_REF: defined_type = 15; pub const DEFINED_FUNC: defined_type = 16; pub const DEFINED_CONST_FROM: defined_type = 17; pub type defined_type = u32; -pub const RB_INVALID_SHAPE_ID: _bindgen_ty_38 = 4294967295; -pub type _bindgen_ty_38 = u32; +pub const RB_INVALID_SHAPE_ID: zjit_exported_constants = 4294967295; +pub type zjit_exported_constants = u32; pub const ROBJECT_OFFSET_AS_HEAP_FIELDS: robject_offsets = 16; pub const ROBJECT_OFFSET_AS_ARY: robject_offsets = 16; pub type robject_offsets = u32; From 3c16f321cb217e53e3e4aa9205525c4775f47a44 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Oct 2025 09:55:12 +0200 Subject: [PATCH 03/14] ZJIT: Add default FnProperties for unknown functions --- zjit/src/hir.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 248c2af7abd5c8..d132e6662dd733 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2342,7 +2342,14 @@ impl Function { use crate::cruby_methods::FnProperties; // Filter for simple call sites (i.e. no splats etc.) // Commit to the replacement. Put PatchPoint. - if let Some(FnProperties { leaf: true, no_gc: true, return_type, elidable }) = ZJITState::get_method_annotations().get_cfunc_properties(method) { + let props = ZJITState::get_method_annotations().get_cfunc_properties(method) + .unwrap_or(FnProperties { leaf: false, + no_gc: false, + return_type: types::BasicObject, + elidable: false }); + if props.leaf && props.no_gc { + let return_type = props.return_type; + let elidable = props.elidable; let ccall = fun.push_insn(block, Insn::CCall { cfunc, args: cfunc_args, name: method_id, return_type, elidable }); fun.make_equal_to(send_insn_id, ccall); } else { From a47048d5cfed9d51115ee91d0b30c5bbd4569abf Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Oct 2025 09:58:23 +0200 Subject: [PATCH 04/14] ZJIT: Add return_type to CCallWithFrame --- zjit/src/hir.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index d132e6662dd733..e247b85bed0d38 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -656,7 +656,8 @@ pub enum Insn { args: Vec, cme: *const rb_callable_method_entry_t, name: ID, - state: InsnId + state: InsnId, + return_type: Type, }, /// Call a variadic C function with signature: func(int argc, VALUE *argv, VALUE recv) @@ -1564,7 +1565,7 @@ impl Function { &ObjectAlloc { val, state } => ObjectAlloc { val: find!(val), state }, &ObjectAllocClass { class, state } => ObjectAllocClass { class, state: find!(state) }, &CCall { cfunc, ref args, name, return_type, elidable } => CCall { cfunc, args: find_vec!(args), name, return_type, elidable }, - &CCallWithFrame { cd, cfunc, ref args, cme, name, state } => CCallWithFrame { cd, cfunc, args: find_vec!(args), cme, name, state: find!(state) }, + &CCallWithFrame { cd, cfunc, ref args, cme, name, state, return_type } => CCallWithFrame { cd, cfunc, args: find_vec!(args), cme, name, state: find!(state), return_type }, &CCallVariadic { cfunc, recv, ref args, cme, name, state } => CCallVariadic { cfunc, recv: find!(recv), args: find_vec!(args), cme, name, state }, @@ -1665,7 +1666,7 @@ impl Function { Insn::NewRangeFixnum { .. } => types::RangeExact, Insn::ObjectAlloc { .. } => types::HeapObject, Insn::ObjectAllocClass { class, .. } => Type::from_class(*class), - Insn::CCallWithFrame { .. } => types::BasicObject, + &Insn::CCallWithFrame { return_type, .. } => return_type, Insn::CCall { return_type, .. } => *return_type, Insn::CCallVariadic { .. } => types::BasicObject, Insn::GuardType { val, guard_type, .. } => self.type_of(*val).intersection(*guard_type), @@ -2347,8 +2348,8 @@ impl Function { no_gc: false, return_type: types::BasicObject, elidable: false }); + let return_type = props.return_type; if props.leaf && props.no_gc { - let return_type = props.return_type; let elidable = props.elidable; let ccall = fun.push_insn(block, Insn::CCall { cfunc, args: cfunc_args, name: method_id, return_type, elidable }); fun.make_equal_to(send_insn_id, ccall); @@ -2356,7 +2357,7 @@ impl Function { if get_option!(stats) { count_not_inlined_cfunc(fun, block, method); } - let ccall = fun.push_insn(block, Insn::CCallWithFrame { cd, cfunc, args: cfunc_args, cme: method, name: method_id, state }); + let ccall = fun.push_insn(block, Insn::CCallWithFrame { cd, cfunc, args: cfunc_args, cme: method, name: method_id, state, return_type }); fun.make_equal_to(send_insn_id, ccall); } From 5c986c7da275df0de3107fbea28e8f1ee592444b Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Oct 2025 10:07:13 +0200 Subject: [PATCH 05/14] ZJIT: Allow no properties to annotate! macro --- zjit/src/cruby_methods.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 41cff3403bcdfa..ecfaadd8e344bc 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -140,11 +140,12 @@ pub fn init() -> Annotations { let builtin_funcs = &mut HashMap::new(); macro_rules! annotate { - ($module:ident, $method_name:literal, $return_type:expr, $($properties:ident),+) => { + ($module:ident, $method_name:literal, $return_type:expr $(, $properties:ident)*) => { + #[allow(unused_mut)] let mut props = FnProperties { no_gc: false, leaf: false, elidable: false, return_type: $return_type }; $( props.$properties = true; - )+ + )* annotate_c_method(cfuncs, unsafe { $module }, $method_name, props); } } From e1c998ab917ba3a319ca0fd32f0857f1dbace906 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Oct 2025 10:07:27 +0200 Subject: [PATCH 06/14] ZJIT: Annotate Array#reverse as returning ArrayExact --- zjit/src/cruby_methods.rs | 1 + zjit/src/hir.rs | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index ecfaadd8e344bc..1df348bc9eef82 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -173,6 +173,7 @@ pub fn init() -> Annotations { annotate!(rb_cArray, "length", types::Fixnum, no_gc, leaf, elidable); annotate!(rb_cArray, "size", types::Fixnum, no_gc, leaf, elidable); annotate!(rb_cArray, "empty?", types::BoolExact, no_gc, leaf, elidable); + annotate!(rb_cArray, "reverse", types::ArrayExact); annotate!(rb_cHash, "empty?", types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cNilClass, "nil?", types::TrueClass, no_gc, leaf, elidable); annotate!(rb_mKernel, "nil?", types::FalseClass, no_gc, leaf, elidable); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index e247b85bed0d38..32024102cf218b 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -12437,4 +12437,28 @@ mod opt_tests { Return v14 "); } + + #[test] + fn test_array_reverse_returns_array() { + eval(r#" + def test = [].reverse + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v11:ArrayExact = NewArray + PatchPoint MethodRedefined(Array@0x1000, reverse@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(Array@0x1000) + v22:ArrayExact = CallCFunc reverse@0x1038, v11 + CheckInterrupts + Return v22 + "); + } } From fc735e257d65ba09ebc62dd698fda35bf4f0a585 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Oct 2025 10:12:46 +0200 Subject: [PATCH 07/14] ZJIT: Allow marking CCallWithFrame elidable Also mark Array#reverse as elidable. --- zjit/src/cruby_methods.rs | 2 +- zjit/src/hir.rs | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 1df348bc9eef82..334d4e2337e60b 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -173,7 +173,7 @@ pub fn init() -> Annotations { annotate!(rb_cArray, "length", types::Fixnum, no_gc, leaf, elidable); annotate!(rb_cArray, "size", types::Fixnum, no_gc, leaf, elidable); annotate!(rb_cArray, "empty?", types::BoolExact, no_gc, leaf, elidable); - annotate!(rb_cArray, "reverse", types::ArrayExact); + annotate!(rb_cArray, "reverse", types::ArrayExact, leaf, elidable); annotate!(rb_cHash, "empty?", types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cNilClass, "nil?", types::TrueClass, no_gc, leaf, elidable); annotate!(rb_mKernel, "nil?", types::FalseClass, no_gc, leaf, elidable); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 32024102cf218b..ec6746ec398c74 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -658,6 +658,7 @@ pub enum Insn { name: ID, state: InsnId, return_type: Type, + elidable: bool, }, /// Call a variadic C function with signature: func(int argc, VALUE *argv, VALUE recv) @@ -846,6 +847,7 @@ impl Insn { Insn::LoadIvarEmbedded { .. } => false, Insn::LoadIvarExtended { .. } => false, Insn::CCall { elidable, .. } => !elidable, + Insn::CCallWithFrame { elidable, .. } => !elidable, Insn::ObjectAllocClass { .. } => false, // TODO: NewRange is effects free if we can prove the two ends to be Fixnum, // but we don't have type information here in `impl Insn`. See rb_range_new(). @@ -1565,7 +1567,7 @@ impl Function { &ObjectAlloc { val, state } => ObjectAlloc { val: find!(val), state }, &ObjectAllocClass { class, state } => ObjectAllocClass { class, state: find!(state) }, &CCall { cfunc, ref args, name, return_type, elidable } => CCall { cfunc, args: find_vec!(args), name, return_type, elidable }, - &CCallWithFrame { cd, cfunc, ref args, cme, name, state, return_type } => CCallWithFrame { cd, cfunc, args: find_vec!(args), cme, name, state: find!(state), return_type }, + &CCallWithFrame { cd, cfunc, ref args, cme, name, state, return_type, elidable } => CCallWithFrame { cd, cfunc, args: find_vec!(args), cme, name, state: find!(state), return_type, elidable }, &CCallVariadic { cfunc, recv, ref args, cme, name, state } => CCallVariadic { cfunc, recv: find!(recv), args: find_vec!(args), cme, name, state }, @@ -2349,15 +2351,15 @@ impl Function { return_type: types::BasicObject, elidable: false }); let return_type = props.return_type; + let elidable = props.elidable; if props.leaf && props.no_gc { - let elidable = props.elidable; let ccall = fun.push_insn(block, Insn::CCall { cfunc, args: cfunc_args, name: method_id, return_type, elidable }); fun.make_equal_to(send_insn_id, ccall); } else { if get_option!(stats) { count_not_inlined_cfunc(fun, block, method); } - let ccall = fun.push_insn(block, Insn::CCallWithFrame { cd, cfunc, args: cfunc_args, cme: method, name: method_id, state, return_type }); + let ccall = fun.push_insn(block, Insn::CCallWithFrame { cd, cfunc, args: cfunc_args, cme: method, name: method_id, state, return_type, elidable }); fun.make_equal_to(send_insn_id, ccall); } @@ -12461,4 +12463,31 @@ mod opt_tests { Return v22 "); } + + #[test] + fn test_array_reverse_is_elidable() { + eval(r#" + def test + [].reverse + 5 + end + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v11:ArrayExact = NewArray + PatchPoint MethodRedefined(Array@0x1000, reverse@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(Array@0x1000) + v16:Fixnum[5] = Const Value(5) + CheckInterrupts + Return v16 + "); + } } From d798e3c46ffb25ec1000893d7e41ea8bc4dffed9 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Oct 2025 10:21:46 +0200 Subject: [PATCH 08/14] ZJIT: Allow annotating CCallVariadic --- zjit/src/codegen.rs | 2 +- zjit/src/cruby_methods.rs | 12 ++++++++++++ zjit/src/hir.rs | 26 ++++++++++++++------------ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index b1a2cb672641ef..4b9331e05b0892 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -409,7 +409,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::CCallWithFrame { cd, state, args, .. } if args.len() > C_ARG_OPNDS.len() => gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::CCallWithFrameTooManyArgs), Insn::CCallWithFrame { cfunc, args, cme, state, .. } => gen_ccall_with_frame(jit, asm, *cfunc, opnds!(args), *cme, &function.frame_state(*state)), - Insn::CCallVariadic { cfunc, recv, args, name: _, cme, state } => { + Insn::CCallVariadic { cfunc, recv, args, name: _, cme, state, return_type: _, elidable: _ } => { gen_ccall_variadic(jit, asm, *cfunc, opnd!(recv), opnds!(args), *cme, &function.frame_state(*state)) } Insn::GetIvar { self_val, id, state: _ } => gen_getivar(asm, opnd!(self_val), *id), diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 334d4e2337e60b..7721ca844035ad 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -31,6 +31,18 @@ pub struct FnProperties { pub elidable: bool, } +/// A safe default for un-annotated Ruby methods: we can't optimize them or their returned values. +impl Default for FnProperties { + fn default() -> Self { + Self { + no_gc: false, + leaf: false, + return_type: types::BasicObject, + elidable: false, + } + } +} + impl Annotations { /// Query about properties of a C method pub fn get_cfunc_properties(&self, method: *const rb_callable_method_entry_t) -> Option { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index ec6746ec398c74..cee546b0261307 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -670,6 +670,8 @@ pub enum Insn { cme: *const rb_callable_method_entry_t, name: ID, state: InsnId, + return_type: Type, + elidable: bool, }, /// Un-optimized fallback implementation (dynamic dispatch) for send-ish instructions @@ -1568,8 +1570,8 @@ impl Function { &ObjectAllocClass { class, state } => ObjectAllocClass { class, state: find!(state) }, &CCall { cfunc, ref args, name, return_type, elidable } => CCall { cfunc, args: find_vec!(args), name, return_type, elidable }, &CCallWithFrame { cd, cfunc, ref args, cme, name, state, return_type, elidable } => CCallWithFrame { cd, cfunc, args: find_vec!(args), cme, name, state: find!(state), return_type, elidable }, - &CCallVariadic { cfunc, recv, ref args, cme, name, state } => CCallVariadic { - cfunc, recv: find!(recv), args: find_vec!(args), cme, name, state + &CCallVariadic { cfunc, recv, ref args, cme, name, state, return_type, elidable } => CCallVariadic { + cfunc, recv: find!(recv), args: find_vec!(args), cme, name, state, return_type, elidable }, &Defined { op_type, obj, pushval, v, state } => Defined { op_type, obj, pushval, v: find!(v), state: find!(state) }, &DefinedIvar { self_val, pushval, id, state } => DefinedIvar { self_val: find!(self_val), pushval, id, state }, @@ -1670,7 +1672,7 @@ impl Function { Insn::ObjectAllocClass { class, .. } => Type::from_class(*class), &Insn::CCallWithFrame { return_type, .. } => return_type, Insn::CCall { return_type, .. } => *return_type, - Insn::CCallVariadic { .. } => types::BasicObject, + &Insn::CCallVariadic { return_type, .. } => return_type, Insn::GuardType { val, guard_type, .. } => self.type_of(*val).intersection(*guard_type), Insn::GuardTypeNot { .. } => types::BasicObject, Insn::GuardBitEquals { val, expected, .. } => self.type_of(*val).intersection(Type::from_value(*expected)), @@ -2325,10 +2327,12 @@ impl Function { let ci_flags = unsafe { vm_ci_flag(call_info) }; + // Filter for simple call sites (i.e. no splats etc.) if ci_flags & VM_CALL_ARGS_SIMPLE == 0 { return Err(()); } + // Commit to the replacement. Put PatchPoint. gen_patch_points_for_optimized_ccall(fun, block, recv_class, method_id, method, state); if recv_class.instance_can_have_singleton_class() { fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass: recv_class }, state }); @@ -2341,17 +2345,10 @@ impl Function { let mut cfunc_args = vec![recv]; cfunc_args.append(&mut args); - // Filter for a leaf and GC free function - use crate::cruby_methods::FnProperties; - // Filter for simple call sites (i.e. no splats etc.) - // Commit to the replacement. Put PatchPoint. - let props = ZJITState::get_method_annotations().get_cfunc_properties(method) - .unwrap_or(FnProperties { leaf: false, - no_gc: false, - return_type: types::BasicObject, - elidable: false }); + let props = ZJITState::get_method_annotations().get_cfunc_properties(method).unwrap_or_default(); let return_type = props.return_type; let elidable = props.elidable; + // Filter for a leaf and GC free function if props.leaf && props.no_gc { let ccall = fun.push_insn(block, Insn::CCall { cfunc, args: cfunc_args, name: method_id, return_type, elidable }); fun.make_equal_to(send_insn_id, ccall); @@ -2385,6 +2382,9 @@ impl Function { } let cfunc = unsafe { get_mct_func(cfunc) }.cast(); + let props = ZJITState::get_method_annotations().get_cfunc_properties(method).unwrap_or_default(); + let return_type = props.return_type; + let elidable = props.elidable; let ccall = fun.push_insn(block, Insn::CCallVariadic { cfunc, recv, @@ -2392,6 +2392,8 @@ impl Function { cme: method, name: method_id, state, + return_type, + elidable, }); fun.make_equal_to(send_insn_id, ccall); From 9020341bb4847d21339f6f45dceb2e498efd002c Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Oct 2025 10:23:15 +0200 Subject: [PATCH 09/14] ZJIT: Annotate Array#join as returning StringExact --- zjit/src/cruby_methods.rs | 1 + zjit/src/hir.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 7721ca844035ad..5cb42291b02b22 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -186,6 +186,7 @@ pub fn init() -> Annotations { annotate!(rb_cArray, "size", types::Fixnum, no_gc, leaf, elidable); annotate!(rb_cArray, "empty?", types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cArray, "reverse", types::ArrayExact, leaf, elidable); + annotate!(rb_cArray, "join", types::StringExact); annotate!(rb_cHash, "empty?", types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cNilClass, "nil?", types::TrueClass, no_gc, leaf, elidable); annotate!(rb_mKernel, "nil?", types::FalseClass, no_gc, leaf, elidable); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index cee546b0261307..5a8ebaf2f26d61 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -12492,4 +12492,30 @@ mod opt_tests { Return v16 "); } + + #[test] + fn test_array_join_returns_string() { + eval(r#" + def test = [].join "," + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v11:ArrayExact = NewArray + v12:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v14:StringExact = StringCopy v12 + PatchPoint MethodRedefined(Array@0x1008, join@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(Array@0x1008) + v25:StringExact = CCallVariadic join@0x1040, v11, v14 + CheckInterrupts + Return v25 + "); + } } From 6a25a8b1e28cc5c1b28fc12717a0fade4da23a7c Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Oct 2025 10:38:26 +0200 Subject: [PATCH 10/14] ZJIT: Get stats for which C functions are not annotated --- zjit.rb | 1 + zjit/src/hir.rs | 25 +++++++++++++++++++++++-- zjit/src/state.rs | 9 +++++++++ zjit/src/stats.rs | 7 +++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/zjit.rb b/zjit.rb index 75c57f9a35c195..87ff52f55a1c21 100644 --- a/zjit.rb +++ b/zjit.rb @@ -156,6 +156,7 @@ def stats_string # Show counters independent from exit_* or dynamic_send_* print_counters_with_prefix(prefix: 'not_inlined_cfuncs_', prompt: 'not inlined C methods', buf:, stats:, limit: 20) + print_counters_with_prefix(prefix: 'not_annotated_cfuncs_', prompt: 'not annotated C methods', buf:, stats:, limit: 20) # Show fallback counters, ordered by the typical amount of fallbacks for the prefix at the time print_counters_with_prefix(prefix: 'unspecialized_def_type_', prompt: 'not optimized method types', buf:, stats:, limit: 20) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 5a8ebaf2f26d61..23f4e828c821eb 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2345,7 +2345,11 @@ impl Function { let mut cfunc_args = vec![recv]; cfunc_args.append(&mut args); - let props = ZJITState::get_method_annotations().get_cfunc_properties(method).unwrap_or_default(); + let props = ZJITState::get_method_annotations().get_cfunc_properties(method); + if props.is_none() && get_option!(stats) { + count_not_annotated_cfunc(fun, block, method); + } + let props = props.unwrap_or_default(); let return_type = props.return_type; let elidable = props.elidable; // Filter for a leaf and GC free function @@ -2382,7 +2386,11 @@ impl Function { } let cfunc = unsafe { get_mct_func(cfunc) }.cast(); - let props = ZJITState::get_method_annotations().get_cfunc_properties(method).unwrap_or_default(); + let props = ZJITState::get_method_annotations().get_cfunc_properties(method); + if props.is_none() && get_option!(stats) { + count_not_annotated_cfunc(fun, block, method); + } + let props = props.unwrap_or_default(); let return_type = props.return_type; let elidable = props.elidable; let ccall = fun.push_insn(block, Insn::CCallVariadic { @@ -2426,6 +2434,19 @@ impl Function { fun.push_insn(block, Insn::IncrCounterPtr { counter_ptr }); } + fn count_not_annotated_cfunc(fun: &mut Function, block: BlockId, cme: *const rb_callable_method_entry_t) { + let owner = unsafe { (*cme).owner }; + let called_id = unsafe { (*cme).called_id }; + let class_name = get_class_name(owner); + let method_name = called_id.contents_lossy(); + let qualified_method_name = format!("{}#{}", class_name, method_name); + let not_annotated_cfunc_counter_pointers = ZJITState::get_not_annotated_cfunc_counter_pointers(); + let counter_ptr = not_annotated_cfunc_counter_pointers.entry(qualified_method_name.clone()).or_insert_with(|| Box::new(0)); + let counter_ptr = &mut **counter_ptr as *mut u64; + + fun.push_insn(block, Insn::IncrCounterPtr { counter_ptr }); + } + for block in self.rpo() { let old_insns = std::mem::take(&mut self.blocks[block.0].insns); assert!(self.blocks[block.0].insns.is_empty()); diff --git a/zjit/src/state.rs b/zjit/src/state.rs index 409cac7e9bb421..1b766d5bc4b5aa 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -54,6 +54,9 @@ pub struct ZJITState { /// Counter pointers for full frame C functions full_frame_cfunc_counter_pointers: HashMap>, + /// Counter pointers for un-annotated C functions + not_annotated_frame_cfunc_counter_pointers: HashMap>, + /// Locations of side exists within generated code exit_locations: Option, } @@ -98,6 +101,7 @@ impl ZJITState { function_stub_hit_trampoline, exit_trampoline_with_counter: exit_trampoline, full_frame_cfunc_counter_pointers: HashMap::new(), + not_annotated_frame_cfunc_counter_pointers: HashMap::new(), exit_locations, }; unsafe { ZJIT_STATE = Some(zjit_state); } @@ -167,6 +171,11 @@ impl ZJITState { &mut ZJITState::get_instance().full_frame_cfunc_counter_pointers } + /// Get a mutable reference to non-annotated cfunc counter pointers + pub fn get_not_annotated_cfunc_counter_pointers() -> &'static mut HashMap> { + &mut ZJITState::get_instance().not_annotated_frame_cfunc_counter_pointers + } + /// Was --zjit-save-compiled-iseqs specified? pub fn should_log_compiled_iseqs() -> bool { get_option!(log_compiled_iseqs).is_some() diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index a9b7270444a7a5..6898053dca789b 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -480,6 +480,13 @@ pub extern "C" fn rb_zjit_stats(_ec: EcPtr, _self: VALUE, target_key: VALUE) -> set_stat_usize!(hash, &key_string, **counter); } + // Set not annotated cfunc counters + let not_annotated_cfuncs = ZJITState::get_not_annotated_cfunc_counter_pointers(); + for (signature, counter) in not_annotated_cfuncs.iter() { + let key_string = format!("not_annotated_cfuncs_{}", signature); + set_stat_usize!(hash, &key_string, **counter); + } + hash } From d25d993aa32cb963d9d4d9d9a8220c1fc69e1e19 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Oct 2025 10:44:02 +0200 Subject: [PATCH 11/14] ZJIT: Annotate String#to_s as returning StringExact --- zjit/src/cruby_methods.rs | 1 + zjit/src/hir.rs | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 5cb42291b02b22..7467cb50c85446 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -180,6 +180,7 @@ pub fn init() -> Annotations { annotate!(rb_mKernel, "itself", types::BasicObject, no_gc, leaf, elidable); annotate!(rb_cString, "bytesize", types::Fixnum, no_gc, leaf); + annotate!(rb_cString, "to_s", types::StringExact); annotate!(rb_cModule, "name", types::StringExact.union(types::NilClass), no_gc, leaf, elidable); annotate!(rb_cModule, "===", types::BoolExact, no_gc, leaf); annotate!(rb_cArray, "length", types::Fixnum, no_gc, leaf, elidable); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 23f4e828c821eb..f8e8c7539d92e3 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -12539,4 +12539,29 @@ mod opt_tests { Return v25 "); } + + #[test] + fn test_string_to_s_returns_string() { + eval(r#" + def test = "".to_s + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v12:StringExact = StringCopy v10 + PatchPoint MethodRedefined(String@0x1008, to_s@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(String@0x1008) + v23:StringExact = CallCFunc to_s@0x1040, v12 + CheckInterrupts + Return v23 + "); + } } From 117e5b68c84f48c22057f19fe60ed5468c988b76 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Oct 2025 10:47:57 +0200 Subject: [PATCH 12/14] ZJIT: Print CCallWithFrame as CCallWithFrame, not CallCFunc --- zjit/src/hir.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index f8e8c7539d92e3..f9283cba79b6c2 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1044,7 +1044,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Ok(()) }, Insn::CCallWithFrame { cfunc, args, name, .. } => { - write!(f, "CallCFunc {}@{:p}", name.contents_lossy(), self.ptr_map.map_ptr(cfunc))?; + write!(f, "CCallWithFrame {}@{:p}", name.contents_lossy(), self.ptr_map.map_ptr(cfunc))?; for arg in args { write!(f, ", {arg}")?; } @@ -10985,7 +10985,7 @@ mod opt_tests { v11:HashExact = NewHash PatchPoint MethodRedefined(Hash@0x1000, dup@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Hash@0x1000) - v24:BasicObject = CallCFunc dup@0x1038, v11 + v24:BasicObject = CCallWithFrame dup@0x1038, v11 v15:BasicObject = SendWithoutBlock v24, :freeze CheckInterrupts Return v15 @@ -11078,7 +11078,7 @@ mod opt_tests { v11:ArrayExact = NewArray PatchPoint MethodRedefined(Array@0x1000, dup@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) - v24:BasicObject = CallCFunc dup@0x1038, v11 + v24:BasicObject = CCallWithFrame dup@0x1038, v11 v15:BasicObject = SendWithoutBlock v24, :freeze CheckInterrupts Return v15 @@ -11172,7 +11172,7 @@ mod opt_tests { v12:StringExact = StringCopy v10 PatchPoint MethodRedefined(String@0x1008, dup@0x1010, cme:0x1018) PatchPoint NoSingletonClass(String@0x1008) - v25:BasicObject = CallCFunc dup@0x1040, v12 + v25:BasicObject = CCallWithFrame dup@0x1040, v12 v16:BasicObject = SendWithoutBlock v25, :freeze CheckInterrupts Return v16 @@ -11267,7 +11267,7 @@ mod opt_tests { v12:StringExact = StringCopy v10 PatchPoint MethodRedefined(String@0x1008, dup@0x1010, cme:0x1018) PatchPoint NoSingletonClass(String@0x1008) - v25:BasicObject = CallCFunc dup@0x1040, v12 + v25:BasicObject = CCallWithFrame dup@0x1040, v12 v16:BasicObject = SendWithoutBlock v25, :-@ CheckInterrupts Return v16 @@ -11409,7 +11409,7 @@ mod opt_tests { PatchPoint MethodRedefined(Array@0x1008, to_s@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Array@0x1008) v30:ArrayExact = GuardType v9, ArrayExact - v31:BasicObject = CallCFunc to_s@0x1040, v30 + v31:BasicObject = CCallWithFrame to_s@0x1040, v30 v17:String = AnyToString v9, str: v31 v19:StringExact = StringConcat v13, v17 CheckInterrupts @@ -12481,7 +12481,7 @@ mod opt_tests { v11:ArrayExact = NewArray PatchPoint MethodRedefined(Array@0x1000, reverse@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) - v22:ArrayExact = CallCFunc reverse@0x1038, v11 + v22:ArrayExact = CCallWithFrame reverse@0x1038, v11 CheckInterrupts Return v22 "); @@ -12559,7 +12559,7 @@ mod opt_tests { v12:StringExact = StringCopy v10 PatchPoint MethodRedefined(String@0x1008, to_s@0x1010, cme:0x1018) PatchPoint NoSingletonClass(String@0x1008) - v23:StringExact = CallCFunc to_s@0x1040, v12 + v23:StringExact = CCallWithFrame to_s@0x1040, v12 CheckInterrupts Return v23 "); From 14716a02e927880ac7bb45802c1be0c38ab257f0 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 9 Oct 2025 13:02:10 -0400 Subject: [PATCH 13/14] ZJIT: Use clang-16 for bindgen on CI Since many of us developing ZJIT are on at least Clang 16 locally now due to recent macOS update, let's use Clang 16 on CI, too. There is some differences between https://github.com/llvm/llvm-project Clang and Apple Clang, even when the version number match, but I figure it's good to shrink the difference anyways. --- .github/workflows/zjit-ubuntu.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index a30ff1df88fdd9..02afda470aec47 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -69,8 +69,9 @@ jobs: - test_task: 'zjit-bindgen' hint: 'To fix: use patch in logs' - configure: '--enable-zjit=dev --with-gcc=clang-14' - libclang_path: '/usr/lib/llvm-14/lib/libclang.so.1' + configure: '--enable-zjit=dev --with-gcc=clang-16' + clang_path: '/usr/bin/clang-16' + runs-on: 'ubuntu-24.04' # for clang-16 - test_task: 'test-bundled-gems' configure: '--enable-zjit=dev' @@ -87,7 +88,7 @@ jobs: RUST_BACKTRACE: 1 ZJIT_RB_BUG: 1 - runs-on: ubuntu-22.04 + runs-on: ${{ matrix.runs-on || 'ubuntu-22.04' }} if: >- ${{!(false @@ -175,7 +176,7 @@ jobs: PRECHECK_BUNDLED_GEMS: 'no' SYNTAX_SUGGEST_TIMEOUT: '5' ZJIT_BINDGEN_DIFF_OPTS: '--exit-code' - LIBCLANG_PATH: ${{ matrix.libclang_path }} + CLANG_PATH: ${{ matrix.clang_path }} TESTS: ${{ matrix.test_all_opts }} continue-on-error: ${{ matrix.continue-on-test_task || false }} From b999ca0fce8116e9a218731bbbc171a849e53a86 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Oct 2025 19:25:08 +0200 Subject: [PATCH 14/14] ZJIT: Fix land race --- zjit/src/hir.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index f9283cba79b6c2..2fe8eb79700349 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -9153,7 +9153,7 @@ mod opt_tests { PatchPoint MethodRedefined(Hash@0x1000, []@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Hash@0x1000) v26:HashExact = GuardType v9, HashExact - v27:BasicObject = CallCFunc []@0x1038, v26, v13 + v27:BasicObject = CCallWithFrame []@0x1038, v26, v13 CheckInterrupts Return v27 ");