From 0594646c0b6c94cb25f86445582623c9c98ea900 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 17 Oct 2025 15:33:11 -0400 Subject: [PATCH 1/7] ZJIT: Don't push frame for Hash#size (#14871) `Hash#size` was not in "Top-20 not annotated C methods" on lobsters so our before / after benchmarks are not very helpful for this change.
Before
``` ***ZJIT: Printing ZJIT statistics on exit*** Top-20 not inlined C methods (60.9% of total 10,963,289): Kernel#is_a?: 1,047,725 ( 9.6%) String#<<: 861,497 ( 7.9%) Hash#[]=: 740,725 ( 6.8%) Regexp#match?: 398,297 ( 3.6%) String#empty?: 354,809 ( 3.2%) Hash#key?: 349,173 ( 3.2%) String#start_with?: 337,387 ( 3.1%) Kernel#respond_to?: 321,134 ( 2.9%) TrueClass#===: 239,657 ( 2.2%) ObjectSpace::WeakKeyMap#[]: 238,988 ( 2.2%) FalseClass#===: 234,777 ( 2.1%) Array#include?: 213,229 ( 1.9%) Kernel#block_given?: 181,801 ( 1.7%) Kernel#dup: 179,349 ( 1.6%) Kernel#kind_of?: 174,710 ( 1.6%) BasicObject#!=: 174,448 ( 1.6%) String.new: 167,716 ( 1.5%) Hash#fetch: 160,704 ( 1.5%) String#==: 158,858 ( 1.4%) Process.clock_gettime: 145,002 ( 1.3%) Top-20 not annotated C methods (61.8% of total 11,128,431): Kernel#is_a?: 1,226,218 (11.0%) String#<<: 861,497 ( 7.7%) Hash#[]=: 740,904 ( 6.7%) Regexp#match?: 398,297 ( 3.6%) String#empty?: 362,047 ( 3.3%) Hash#key?: 349,173 ( 3.1%) String#start_with?: 337,387 ( 3.0%) Kernel#respond_to?: 321,134 ( 2.9%) TrueClass#===: 239,657 ( 2.2%) ObjectSpace::WeakKeyMap#[]: 238,988 ( 2.1%) FalseClass#===: 234,777 ( 2.1%) Array#include?: 213,229 ( 1.9%) Kernel#block_given?: 191,670 ( 1.7%) Kernel#dup: 179,356 ( 1.6%) Kernel#kind_of?: 174,745 ( 1.6%) BasicObject#!=: 174,632 ( 1.6%) String.new: 167,716 ( 1.5%) String#==: 164,789 ( 1.5%) Hash#fetch: 160,704 ( 1.4%) Process.clock_gettime: 145,002 ( 1.3%) Top-2 not optimized method types for send (100.0% of total 62,854): cfunc: 47,647 (75.8%) iseq: 15,207 (24.2%) Top-6 not optimized method types for send_without_block (100.0% of total 4,497,956): iseq: 2,236,049 (49.7%) bmethod: 993,299 (22.1%) optimized: 949,781 (21.1%) alias: 313,166 ( 7.0%) null: 5,106 ( 0.1%) cfunc: 555 ( 0.0%) Top-13 not optimized instructions (100.0% of total 4,255,830): invokesuper: 2,371,027 (55.7%) invokeblock: 811,314 (19.1%) sendforward: 506,486 (11.9%) opt_eq: 415,294 ( 9.8%) opt_plus: 77,090 ( 1.8%) opt_minus: 36,228 ( 0.9%) opt_send_without_block: 20,297 ( 0.5%) opt_neq: 7,248 ( 0.2%) opt_mult: 6,754 ( 0.2%) opt_or: 3,617 ( 0.1%) opt_lt: 348 ( 0.0%) opt_ge: 91 ( 0.0%) opt_gt: 36 ( 0.0%) Top-9 send fallback reasons (100.0% of total 24,945,472): send_without_block_polymorphic: 9,308,731 (37.3%) send_no_profiles: 5,907,934 (23.7%) send_without_block_not_optimized_method_type: 4,497,956 (18.0%) not_optimized_instruction: 4,255,830 (17.1%) send_without_block_no_profiles: 887,000 ( 3.6%) send_not_optimized_method_type: 62,854 ( 0.3%) send_without_block_cfunc_array_variadic: 15,138 ( 0.1%) obj_to_string_not_string: 9,767 ( 0.0%) send_without_block_direct_too_many_args: 262 ( 0.0%) Top-9 unhandled YARV insns (100.0% of total 707,558): expandarray: 347,142 (49.1%) checkkeyword: 190,708 (27.0%) getclassvariable: 59,296 ( 8.4%) getblockparam: 49,122 ( 6.9%) invokesuperforward: 48,163 ( 6.8%) opt_duparray_send: 11,978 ( 1.7%) getconstant: 840 ( 0.1%) checkmatch: 290 ( 0.0%) once: 19 ( 0.0%) Top-2 compile error reasons (100.0% of total 3,649,990): register_spill_on_alloc: 3,428,507 (93.9%) register_spill_on_ccall: 221,483 ( 6.1%) Top-17 side exit reasons (100.0% of total 10,833,336): compile_error: 3,649,990 (33.7%) guard_type_failure: 2,681,177 (24.7%) guard_shape_failure: 1,897,864 (17.5%) unhandled_yarv_insn: 707,558 ( 6.5%) block_param_proxy_not_iseq_or_ifunc: 536,761 ( 5.0%) unhandled_kwarg: 456,394 ( 4.2%) unknown_newarray_send: 314,671 ( 2.9%) patchpoint_stable_constant_names: 229,825 ( 2.1%) unhandled_splat: 129,577 ( 1.2%) patchpoint_no_singleton_class: 108,465 ( 1.0%) unhandled_hir_insn: 76,401 ( 0.7%) patchpoint_method_redefined: 20,493 ( 0.2%) block_param_proxy_modified: 20,204 ( 0.2%) patchpoint_no_ep_escape: 3,765 ( 0.0%) obj_to_string_fallback: 156 ( 0.0%) guard_type_not_failure: 22 ( 0.0%) interrupt: 13 ( 0.0%) send_count: 67,968,616 dynamic_send_count: 24,945,472 (36.7%) optimized_send_count: 43,023,144 (63.3%) iseq_optimized_send_count: 18,621,234 (27.4%) inline_cfunc_optimized_send_count: 13,438,621 (19.8%) non_variadic_cfunc_optimized_send_count: 8,333,523 (12.3%) variadic_cfunc_optimized_send_count: 2,629,766 ( 3.9%) dynamic_getivar_count: 7,351,238 dynamic_setivar_count: 7,267,701 compiled_iseq_count: 4,772 failed_iseq_count: 465 compile_time: 7,006ms profile_time: 52ms gc_time: 46ms invalidation_time: 123ms vm_write_pc_count: 63,668,147 vm_write_sp_count: 62,343,075 vm_write_locals_count: 62,343,075 vm_write_stack_count: 62,343,075 vm_write_to_parent_iseq_local_count: 292,130 vm_read_from_parent_iseq_local_count: 6,623,223 code_region_bytes: 22,724,608 side_exit_count: 10,833,336 total_insn_count: 519,162,657 vm_insn_count: 164,942,584 zjit_insn_count: 354,220,073 ratio_in_zjit: 68.2% ```
After
``` ***ZJIT: Printing ZJIT statistics on exit*** Top-20 not inlined C methods (61.1% of total 10,915,774): Kernel#is_a?: 1,027,957 ( 9.4%) String#<<: 851,954 ( 7.8%) Hash#[]=: 740,863 ( 6.8%) Regexp#match?: 398,265 ( 3.6%) String#empty?: 353,775 ( 3.2%) Hash#key?: 349,161 ( 3.2%) String#start_with?: 337,386 ( 3.1%) Kernel#respond_to?: 316,003 ( 2.9%) ObjectSpace::WeakKeyMap#[]: 238,978 ( 2.2%) TrueClass#===: 235,771 ( 2.2%) FalseClass#===: 231,144 ( 2.1%) Array#include?: 211,340 ( 1.9%) Hash#fetch: 204,703 ( 1.9%) Kernel#block_given?: 181,791 ( 1.7%) Kernel#dup: 179,337 ( 1.6%) BasicObject#!=: 174,430 ( 1.6%) String.new: 166,696 ( 1.5%) Kernel#kind_of?: 165,600 ( 1.5%) String#==: 154,751 ( 1.4%) Process.clock_gettime: 144,992 ( 1.3%) Top-20 not annotated C methods (62.0% of total 11,078,184): Kernel#is_a?: 1,209,975 (10.9%) String#<<: 851,954 ( 7.7%) Hash#[]=: 741,042 ( 6.7%) Regexp#match?: 398,265 ( 3.6%) String#empty?: 361,013 ( 3.3%) Hash#key?: 349,161 ( 3.2%) String#start_with?: 337,386 ( 3.0%) Kernel#respond_to?: 316,003 ( 2.9%) ObjectSpace::WeakKeyMap#[]: 238,978 ( 2.2%) TrueClass#===: 235,771 ( 2.1%) FalseClass#===: 231,144 ( 2.1%) Array#include?: 211,340 ( 1.9%) Hash#fetch: 204,703 ( 1.8%) Kernel#block_given?: 191,660 ( 1.7%) Kernel#dup: 179,344 ( 1.6%) BasicObject#!=: 174,614 ( 1.6%) String.new: 166,696 ( 1.5%) Kernel#kind_of?: 165,634 ( 1.5%) String#==: 160,682 ( 1.5%) Process.clock_gettime: 144,992 ( 1.3%) Top-2 not optimized method types for send (100.0% of total 71,084): cfunc: 47,638 (67.0%) iseq: 23,446 (33.0%) Top-6 not optimized method types for send_without_block (100.0% of total 4,469,252): iseq: 2,217,500 (49.6%) bmethod: 985,636 (22.1%) optimized: 949,705 (21.2%) alias: 310,751 ( 7.0%) null: 5,106 ( 0.1%) cfunc: 554 ( 0.0%) Top-13 not optimized instructions (100.0% of total 4,264,988): invokesuper: 2,346,307 (55.0%) invokeblock: 809,211 (19.0%) sendforward: 505,452 (11.9%) opt_eq: 454,244 (10.7%) opt_plus: 74,059 ( 1.7%) opt_minus: 36,228 ( 0.8%) opt_send_without_block: 21,396 ( 0.5%) opt_neq: 7,247 ( 0.2%) opt_mult: 6,752 ( 0.2%) opt_or: 3,617 ( 0.1%) opt_lt: 348 ( 0.0%) opt_ge: 91 ( 0.0%) opt_gt: 36 ( 0.0%) Top-9 send fallback reasons (100.0% of total 25,044,791): send_without_block_polymorphic: 9,439,021 (37.7%) send_no_profiles: 5,892,924 (23.5%) send_without_block_not_optimized_method_type: 4,469,252 (17.8%) not_optimized_instruction: 4,264,988 (17.0%) send_without_block_no_profiles: 882,357 ( 3.5%) send_not_optimized_method_type: 71,084 ( 0.3%) send_without_block_cfunc_array_variadic: 15,136 ( 0.1%) obj_to_string_not_string: 9,767 ( 0.0%) send_without_block_direct_too_many_args: 262 ( 0.0%) Top-9 unhandled YARV insns (100.0% of total 688,760): expandarray: 328,369 (47.7%) checkkeyword: 190,697 (27.7%) getclassvariable: 59,286 ( 8.6%) getblockparam: 49,119 ( 7.1%) invokesuperforward: 48,162 ( 7.0%) opt_duparray_send: 11,978 ( 1.7%) getconstant: 840 ( 0.1%) checkmatch: 290 ( 0.0%) once: 19 ( 0.0%) Top-2 compile error reasons (100.0% of total 3,642,051): register_spill_on_alloc: 3,420,578 (93.9%) register_spill_on_ccall: 221,473 ( 6.1%) Top-17 side exit reasons (100.0% of total 10,740,844): compile_error: 3,642,051 (33.9%) guard_type_failure: 2,624,731 (24.4%) guard_shape_failure: 1,902,123 (17.7%) unhandled_yarv_insn: 688,760 ( 6.4%) block_param_proxy_not_iseq_or_ifunc: 534,951 ( 5.0%) unhandled_kwarg: 455,354 ( 4.2%) unknown_newarray_send: 314,667 ( 2.9%) patchpoint_stable_constant_names: 227,790 ( 2.1%) unhandled_splat: 121,916 ( 1.1%) patchpoint_no_singleton_class: 108,465 ( 1.0%) unhandled_hir_insn: 76,397 ( 0.7%) patchpoint_method_redefined: 20,487 ( 0.2%) block_param_proxy_modified: 19,193 ( 0.2%) patchpoint_no_ep_escape: 3,765 ( 0.0%) obj_to_string_fallback: 156 ( 0.0%) guard_type_not_failure: 22 ( 0.0%) interrupt: 16 ( 0.0%) send_count: 67,576,368 dynamic_send_count: 25,044,791 (37.1%) optimized_send_count: 42,531,577 (62.9%) iseq_optimized_send_count: 18,461,332 (27.3%) inline_cfunc_optimized_send_count: 13,154,471 (19.5%) non_variadic_cfunc_optimized_send_count: 8,243,438 (12.2%) variadic_cfunc_optimized_send_count: 2,672,336 ( 4.0%) dynamic_getivar_count: 7,322,001 dynamic_setivar_count: 7,230,445 compiled_iseq_count: 4,771 failed_iseq_count: 466 compile_time: 7,134ms profile_time: 52ms gc_time: 46ms invalidation_time: 123ms vm_write_pc_count: 63,337,758 vm_write_sp_count: 62,014,782 vm_write_locals_count: 62,014,782 vm_write_stack_count: 62,014,782 vm_write_to_parent_iseq_local_count: 292,458 vm_read_from_parent_iseq_local_count: 6,589,698 code_region_bytes: 22,724,608 side_exit_count: 10,740,844 total_insn_count: 515,656,824 vm_insn_count: 163,676,059 zjit_insn_count: 351,980,765 ratio_in_zjit: 68.3% ```
--- zjit/src/cruby_methods.rs | 1 + zjit/src/hir.rs | 57 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index f6c439ad985783..0609d37b1b5854 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -201,6 +201,7 @@ pub fn init() -> Annotations { annotate!(rb_cArray, "join", types::StringExact); annotate!(rb_cArray, "[]", inline_array_aref); annotate!(rb_cHash, "[]", inline_hash_aref); + annotate!(rb_cHash, "size", types::Fixnum, no_gc, 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 f0ada408e36793..eca0e3a598865b 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -13390,4 +13390,61 @@ mod opt_tests { Return v13 "); } + + #[test] + fn test_specialize_hash_size() { + eval(" + def test(hash) = hash.size + test({foo: 3, bar: 1, baz: 4}) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + 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): + PatchPoint MethodRedefined(Hash@0x1000, size@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(Hash@0x1000) + v25:HashExact = GuardType v9, HashExact + IncrCounter inline_cfunc_optimized_send_count + v27:Fixnum = CCall size@0x1038, v25 + CheckInterrupts + Return v27 + "); + } + + #[test] + fn test_eliminate_hash_size() { + eval(" + def test(hash) + hash.size + 5 + end + test({foo: 3, bar: 1, baz: 4}) + "); + 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): + PatchPoint MethodRedefined(Hash@0x1000, size@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(Hash@0x1000) + v28:HashExact = GuardType v9, HashExact + IncrCounter inline_cfunc_optimized_send_count + v19:Fixnum[5] = Const Value(5) + CheckInterrupts + Return v19 + "); + } } From 23287c45806cac060ed63e87176d1a87968b9267 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 17 Oct 2025 15:48:54 -0400 Subject: [PATCH 2/7] ZJIT: Mark commonly-edited files as merge=union (#14865) This helps the default merge driver make reasonable decisions (and therefore avoid conflicts) when multiple people are e.g. adding tests to src/hir.rs at the same time. --- zjit/.gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 zjit/.gitattributes diff --git a/zjit/.gitattributes b/zjit/.gitattributes new file mode 100644 index 00000000000000..2750c4c62683ca --- /dev/null +++ b/zjit/.gitattributes @@ -0,0 +1,3 @@ +src/hir.rs merge=union +src/cruby_methods.rs merge=union +src/codegen.rs merge=union From 1c119f02456ff7dd3a4025456f8de5e50be88ef3 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 17 Oct 2025 16:29:03 -0400 Subject: [PATCH 3/7] Revert "ZJIT: Mark commonly-edited files as merge=union (#14865)" This reverts commit 23287c45806cac060ed63e87176d1a87968b9267. This looks like a mixed bag... --- zjit/.gitattributes | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 zjit/.gitattributes diff --git a/zjit/.gitattributes b/zjit/.gitattributes deleted file mode 100644 index 2750c4c62683ca..00000000000000 --- a/zjit/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -src/hir.rs merge=union -src/cruby_methods.rs merge=union -src/codegen.rs merge=union From cb55043383cbf39ac1df1d227836080a3d7cef33 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Thu, 16 Oct 2025 17:11:30 -0400 Subject: [PATCH 4/7] Set method table owned by iclass in rb_class_duplicate_classext We duplicate the method table in rb_class_duplicate_classext, so we should set RCLASSEXT_ICLASS_IS_ORIGIN and unset RCLASSEXT_ICLASS_ORIGIN_SHARED_MTBL to signal that the iclass owns the method table and it should be freed. --- class.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/class.c b/class.c index 469cc5e54fef86..c8bf624140d726 100644 --- a/class.c +++ b/class.c @@ -305,6 +305,8 @@ rb_class_duplicate_classext(rb_classext_t *orig, VALUE klass, const rb_namespace RCLASSEXT_SUPER(ext) = RCLASSEXT_SUPER(orig); RCLASSEXT_M_TBL(ext) = duplicate_classext_m_tbl(RCLASSEXT_M_TBL(orig), klass, dup_iclass); + RCLASSEXT_ICLASS_IS_ORIGIN(ext) = true; + RCLASSEXT_ICLASS_ORIGIN_SHARED_MTBL(ext) = false; if (orig->fields_obj) { RB_OBJ_WRITE(klass, &ext->fields_obj, rb_imemo_fields_clone(orig->fields_obj)); From a0bf6d349856dfca22798a49c5b4e05162edaf3c Mon Sep 17 00:00:00 2001 From: Aiden Fox Ivey Date: Fri, 17 Oct 2025 18:37:22 -0400 Subject: [PATCH 5/7] ZJIT: Add inlining for Kernel#respond_to? (#14873) lobsters before:
``` ***ZJIT: Printing ZJIT statistics on exit*** Top-20 not inlined C methods (61.1% of total 10,568,718): Kernel#is_a?: 1,030,925 ( 9.8%) String#<<: 851,954 ( 8.1%) Hash#[]=: 742,942 ( 7.0%) Regexp#match?: 399,898 ( 3.8%) Hash#key?: 349,146 ( 3.3%) String#start_with?: 334,963 ( 3.2%) Kernel#respond_to?: 316,528 ( 3.0%) ObjectSpace::WeakKeyMap#[]: 238,978 ( 2.3%) TrueClass#===: 235,771 ( 2.2%) FalseClass#===: 231,144 ( 2.2%) Array#include?: 211,386 ( 2.0%) Hash#fetch: 204,702 ( 1.9%) Kernel#block_given?: 181,796 ( 1.7%) Kernel#dup: 179,341 ( 1.7%) BasicObject#!=: 175,997 ( 1.7%) String.new: 166,696 ( 1.6%) Kernel#kind_of?: 165,600 ( 1.6%) String#==: 157,746 ( 1.5%) Process.clock_gettime: 144,992 ( 1.4%) Array#any?: 138,310 ( 1.3%) Top-20 not annotated C methods (62.1% of total 10,723,613): Kernel#is_a?: 1,212,816 (11.3%) String#<<: 851,954 ( 7.9%) Hash#[]=: 743,121 ( 6.9%) Regexp#match?: 399,898 ( 3.7%) Hash#key?: 349,146 ( 3.3%) String#start_with?: 334,963 ( 3.1%) Kernel#respond_to?: 316,528 ( 3.0%) ObjectSpace::WeakKeyMap#[]: 238,978 ( 2.2%) TrueClass#===: 235,771 ( 2.2%) FalseClass#===: 231,144 ( 2.2%) Array#include?: 211,386 ( 2.0%) Hash#fetch: 204,702 ( 1.9%) Kernel#block_given?: 191,665 ( 1.8%) Kernel#dup: 179,348 ( 1.7%) BasicObject#!=: 176,181 ( 1.6%) String.new: 166,696 ( 1.6%) Kernel#kind_of?: 165,634 ( 1.5%) String#==: 163,678 ( 1.5%) Process.clock_gettime: 144,992 ( 1.4%) Array#any?: 138,310 ( 1.3%) Top-2 not optimized method types for send (100.0% of total 72,324): cfunc: 48,057 (66.4%) iseq: 24,267 (33.6%) Top-6 not optimized method types for send_without_block (100.0% of total 4,523,699): iseq: 2,271,952 (50.2%) bmethod: 985,636 (21.8%) optimized: 949,704 (21.0%) alias: 310,747 ( 6.9%) null: 5,106 ( 0.1%) cfunc: 554 ( 0.0%) Top-13 not optimized instructions (100.0% of total 4,293,340): invokesuper: 2,373,561 (55.3%) invokeblock: 811,934 (18.9%) sendforward: 505,452 (11.8%) opt_eq: 451,756 (10.5%) opt_plus: 74,406 ( 1.7%) opt_minus: 36,228 ( 0.8%) opt_send_without_block: 21,792 ( 0.5%) opt_neq: 7,231 ( 0.2%) opt_mult: 6,752 ( 0.2%) opt_or: 3,753 ( 0.1%) opt_lt: 348 ( 0.0%) opt_ge: 91 ( 0.0%) opt_gt: 36 ( 0.0%) Top-9 send fallback reasons (100.0% of total 25,481,476): send_without_block_polymorphic: 9,722,801 (38.2%) send_no_profiles: 5,894,799 (23.1%) send_without_block_not_optimized_method_type: 4,523,699 (17.8%) not_optimized_instruction: 4,293,340 (16.8%) send_without_block_no_profiles: 948,985 ( 3.7%) send_not_optimized_method_type: 72,324 ( 0.3%) send_without_block_cfunc_array_variadic: 15,134 ( 0.1%) obj_to_string_not_string: 9,765 ( 0.0%) send_without_block_direct_too_many_args: 629 ( 0.0%) Top-9 unhandled YARV insns (100.0% of total 690,957): expandarray: 328,491 (47.5%) checkkeyword: 190,694 (27.6%) getclassvariable: 59,907 ( 8.7%) invokesuperforward: 49,503 ( 7.2%) getblockparam: 49,119 ( 7.1%) opt_duparray_send: 11,978 ( 1.7%) getconstant: 952 ( 0.1%) checkmatch: 290 ( 0.0%) once: 23 ( 0.0%) Top-3 compile error reasons (100.0% of total 3,718,841): register_spill_on_alloc: 3,418,472 (91.9%) register_spill_on_ccall: 182,023 ( 4.9%) exception_handler: 118,346 ( 3.2%) Top-17 side exit reasons (100.0% of total 10,861,013): compile_error: 3,718,841 (34.2%) guard_type_failure: 2,638,940 (24.3%) guard_shape_failure: 1,917,541 (17.7%) unhandled_yarv_insn: 690,957 ( 6.4%) block_param_proxy_not_iseq_or_ifunc: 535,789 ( 4.9%) unhandled_kwarg: 455,351 ( 4.2%) unknown_newarray_send: 314,786 ( 2.9%) patchpoint_stable_constant_names: 235,507 ( 2.2%) unhandled_splat: 122,071 ( 1.1%) patchpoint_no_singleton_class: 109,668 ( 1.0%) unhandled_hir_insn: 76,397 ( 0.7%) patchpoint_method_redefined: 21,598 ( 0.2%) block_param_proxy_modified: 19,193 ( 0.2%) patchpoint_no_ep_escape: 3,765 ( 0.0%) obj_to_string_fallback: 568 ( 0.0%) guard_type_not_failure: 22 ( 0.0%) interrupt: 19 ( 0.0%) send_count: 68,205,150 dynamic_send_count: 25,481,476 (37.4%) optimized_send_count: 42,723,674 (62.6%) iseq_optimized_send_count: 18,588,101 (27.3%) inline_cfunc_optimized_send_count: 13,566,855 (19.9%) non_variadic_cfunc_optimized_send_count: 7,904,518 (11.6%) variadic_cfunc_optimized_send_count: 2,664,200 ( 3.9%) dynamic_getivar_count: 7,366,650 dynamic_setivar_count: 7,245,122 compiled_iseq_count: 4,796 failed_iseq_count: 447 compile_time: 778ms profile_time: 9ms gc_time: 11ms invalidation_time: 77ms vm_write_pc_count: 63,636,742 vm_write_sp_count: 62,292,946 vm_write_locals_count: 62,292,946 vm_write_stack_count: 62,292,946 vm_write_to_parent_iseq_local_count: 292,458 vm_read_from_parent_iseq_local_count: 6,600,017 code_region_bytes: 22,970,368 side_exit_count: 10,861,013 total_insn_count: 517,633,620 vm_insn_count: 162,995,567 zjit_insn_count: 354,638,053 ratio_in_zjit: 68.5% ```
lobsters after:
``` ***ZJIT: Printing ZJIT statistics on exit*** Top-20 not inlined C methods (61.1% of total 10,239,008): Kernel#is_a?: 1,030,914 (10.1%) String#<<: 851,954 ( 8.3%) Hash#[]=: 742,942 ( 7.3%) Regexp#match?: 376,144 ( 3.7%) Hash#key?: 349,147 ( 3.4%) String#start_with?: 334,963 ( 3.3%) ObjectSpace::WeakKeyMap#[]: 238,978 ( 2.3%) TrueClass#===: 235,771 ( 2.3%) FalseClass#===: 231,144 ( 2.3%) Array#include?: 211,386 ( 2.1%) Hash#fetch: 204,702 ( 2.0%) Kernel#block_given?: 181,797 ( 1.8%) Kernel#dup: 179,341 ( 1.8%) BasicObject#!=: 175,997 ( 1.7%) String.new: 166,696 ( 1.6%) Kernel#kind_of?: 165,600 ( 1.6%) String#==: 157,751 ( 1.5%) Process.clock_gettime: 144,992 ( 1.4%) Array#any?: 138,311 ( 1.4%) Set#include?: 134,362 ( 1.3%) Top-20 not annotated C methods (62.2% of total 10,372,753): Kernel#is_a?: 1,212,805 (11.7%) String#<<: 851,954 ( 8.2%) Hash#[]=: 743,121 ( 7.2%) Regexp#match?: 376,144 ( 3.6%) Hash#key?: 349,147 ( 3.4%) String#start_with?: 334,963 ( 3.2%) ObjectSpace::WeakKeyMap#[]: 238,978 ( 2.3%) TrueClass#===: 235,771 ( 2.3%) FalseClass#===: 231,144 ( 2.2%) Array#include?: 211,386 ( 2.0%) Hash#fetch: 204,702 ( 2.0%) Kernel#block_given?: 191,666 ( 1.8%) Kernel#dup: 179,348 ( 1.7%) BasicObject#!=: 176,181 ( 1.7%) String.new: 166,696 ( 1.6%) Kernel#kind_of?: 165,634 ( 1.6%) String#==: 163,683 ( 1.6%) Process.clock_gettime: 144,992 ( 1.4%) Array#any?: 138,311 ( 1.3%) Integer#<=>: 135,056 ( 1.3%) Top-2 not optimized method types for send (100.0% of total 72,324): cfunc: 48,057 (66.4%) iseq: 24,267 (33.6%) Top-6 not optimized method types for send_without_block (100.0% of total 4,523,699): iseq: 2,271,952 (50.2%) bmethod: 985,636 (21.8%) optimized: 949,704 (21.0%) alias: 310,747 ( 6.9%) null: 5,106 ( 0.1%) cfunc: 554 ( 0.0%) Top-13 not optimized instructions (100.0% of total 4,293,339): invokesuper: 2,373,561 (55.3%) invokeblock: 811,933 (18.9%) sendforward: 505,452 (11.8%) opt_eq: 451,756 (10.5%) opt_plus: 74,406 ( 1.7%) opt_minus: 36,228 ( 0.8%) opt_send_without_block: 21,792 ( 0.5%) opt_neq: 7,231 ( 0.2%) opt_mult: 6,752 ( 0.2%) opt_or: 3,753 ( 0.1%) opt_lt: 348 ( 0.0%) opt_ge: 91 ( 0.0%) opt_gt: 36 ( 0.0%) Top-9 send fallback reasons (100.0% of total 25,457,719): send_without_block_polymorphic: 9,699,046 (38.1%) send_no_profiles: 5,894,798 (23.2%) send_without_block_not_optimized_method_type: 4,523,699 (17.8%) not_optimized_instruction: 4,293,339 (16.9%) send_without_block_no_profiles: 948,985 ( 3.7%) send_not_optimized_method_type: 72,324 ( 0.3%) send_without_block_cfunc_array_variadic: 15,134 ( 0.1%) obj_to_string_not_string: 9,765 ( 0.0%) send_without_block_direct_too_many_args: 629 ( 0.0%) Top-9 unhandled YARV insns (100.0% of total 690,957): expandarray: 328,491 (47.5%) checkkeyword: 190,694 (27.6%) getclassvariable: 59,907 ( 8.7%) invokesuperforward: 49,503 ( 7.2%) getblockparam: 49,119 ( 7.1%) opt_duparray_send: 11,978 ( 1.7%) getconstant: 952 ( 0.1%) checkmatch: 290 ( 0.0%) once: 23 ( 0.0%) Top-3 compile error reasons (100.0% of total 3,706,981): register_spill_on_alloc: 3,406,595 (91.9%) register_spill_on_ccall: 182,023 ( 4.9%) exception_handler: 118,363 ( 3.2%) Top-17 side exit reasons (100.0% of total 10,837,266): compile_error: 3,706,981 (34.2%) guard_type_failure: 2,638,921 (24.4%) guard_shape_failure: 1,917,552 (17.7%) unhandled_yarv_insn: 690,957 ( 6.4%) block_param_proxy_not_iseq_or_ifunc: 535,789 ( 4.9%) unhandled_kwarg: 455,351 ( 4.2%) unknown_newarray_send: 314,786 ( 2.9%) patchpoint_stable_constant_names: 223,630 ( 2.1%) unhandled_splat: 122,071 ( 1.1%) patchpoint_no_singleton_class: 109,668 ( 1.0%) unhandled_hir_insn: 76,397 ( 0.7%) patchpoint_method_redefined: 21,598 ( 0.2%) block_param_proxy_modified: 19,193 ( 0.2%) patchpoint_no_ep_escape: 3,765 ( 0.0%) obj_to_string_fallback: 568 ( 0.0%) guard_type_not_failure: 22 ( 0.0%) interrupt: 17 ( 0.0%) send_count: 68,157,710 dynamic_send_count: 25,457,719 (37.4%) optimized_send_count: 42,699,991 (62.6%) iseq_optimized_send_count: 18,588,067 (27.3%) inline_cfunc_optimized_send_count: 13,872,916 (20.4%) non_variadic_cfunc_optimized_send_count: 7,904,566 (11.6%) variadic_cfunc_optimized_send_count: 2,334,442 ( 3.4%) dynamic_getivar_count: 7,342,896 dynamic_setivar_count: 7,245,126 compiled_iseq_count: 4,796 failed_iseq_count: 447 compile_time: 791ms profile_time: 9ms gc_time: 9ms invalidation_time: 68ms vm_write_pc_count: 63,283,243 vm_write_sp_count: 61,939,447 vm_write_locals_count: 61,939,447 vm_write_stack_count: 61,939,447 vm_write_to_parent_iseq_local_count: 292,458 vm_read_from_parent_iseq_local_count: 6,576,263 code_region_bytes: 22,872,064 side_exit_count: 10,837,266 total_insn_count: 517,075,555 vm_insn_count: 162,674,783 zjit_insn_count: 354,400,772 ratio_in_zjit: 68.5% ```
--------- Co-authored-by: Max Bernstein --- zjit/src/cruby_methods.rs | 112 +++++++++++++ zjit/src/hir.rs | 341 +++++++++++++++++++++++++++++++++++++- 2 files changed, 452 insertions(+), 1 deletion(-) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 0609d37b1b5854..40fb0cbe442dab 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -205,6 +205,7 @@ pub fn init() -> Annotations { 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); + annotate!(rb_mKernel, "respond_to?", inline_kernel_respond_to_p); annotate!(rb_cBasicObject, "==", types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cBasicObject, "!", types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cBasicObject, "initialize", inline_basic_object_initialize); @@ -290,3 +291,114 @@ fn inline_basic_object_initialize(fun: &mut hir::Function, block: hir::BlockId, let result = fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(Qnil) }); Some(result) } + +fn inline_kernel_respond_to_p( + fun: &mut hir::Function, + block: hir::BlockId, + recv: hir::InsnId, + args: &[hir::InsnId], + state: hir::InsnId, +) -> Option { + // Parse arguments: respond_to?(method_name, allow_priv = false) + let (method_name, allow_priv) = match *args { + [method_name] => (method_name, false), + [method_name, arg] => match fun.type_of(arg) { + t if t.is_known_truthy() => (method_name, true), + t if t.is_known_falsy() => (method_name, false), + // Unknown type; bail out + _ => return None, + }, + // Unknown args; bail out + _ => return None, + }; + + // Method name must be a static symbol + let method_name = fun.type_of(method_name).ruby_object()?; + if !method_name.static_sym_p() { + return None; + } + + // The receiver must have a known class to call `respond_to?` on + // TODO: This is technically overly strict. This would also work if all of the + // observed objects at this point agree on `respond_to?` and we can add many patchpoints. + let recv_class = fun.type_of(recv).runtime_exact_ruby_class()?; + + // Get the method ID and its corresponding callable method entry + let mid = unsafe { rb_sym2id(method_name) }; + let target_cme = unsafe { rb_callable_method_entry_or_negative(recv_class, mid) }; + assert!( + !target_cme.is_null(), + "Should never be null, as in that case we will be returned a \"negative CME\"" + ); + + let cme_def_type = unsafe { get_cme_def_type(target_cme) }; + + // Cannot inline a refined method, since their refinement depends on lexical scope + if cme_def_type == VM_METHOD_TYPE_REFINED { + return None; + } + + let visibility = match cme_def_type { + VM_METHOD_TYPE_UNDEF => METHOD_VISI_UNDEF, + _ => unsafe { METHOD_ENTRY_VISI(target_cme) }, + }; + + let result = match (visibility, allow_priv) { + // Method undefined; check `respond_to_missing?` + (METHOD_VISI_UNDEF, _) => { + let respond_to_missing = ID!(respond_to_missing); + if unsafe { rb_method_basic_definition_p(recv_class, respond_to_missing) } == 0 { + return None; // Custom definition of respond_to_missing?, so cannot inline + } + let respond_to_missing_cme = + unsafe { rb_callable_method_entry(recv_class, respond_to_missing) }; + // Protect against redefinition of `respond_to_missing?` + fun.push_insn( + block, + hir::Insn::PatchPoint { + invariant: hir::Invariant::NoTracePoint, + state, + }, + ); + fun.push_insn( + block, + hir::Insn::PatchPoint { + invariant: hir::Invariant::MethodRedefined { + klass: recv_class, + method: respond_to_missing, + cme: respond_to_missing_cme, + }, + state, + }, + ); + Qfalse + } + // Private method with allow priv=false, so `respond_to?` returns false + (METHOD_VISI_PRIVATE, false) => Qfalse, + // Public method or allow_priv=true: check if implemented + (METHOD_VISI_PUBLIC, _) | (_, true) => { + if cme_def_type == VM_METHOD_TYPE_NOTIMPLEMENTED { + // C method with rb_f_notimplement(). `respond_to?` returns false + // without consulting `respond_to_missing?`. See also: rb_add_method_cfunc() + Qfalse + } else { + Qtrue + } + } + (_, _) => return None, // not public and include_all not known, can't compile + }; + fun.push_insn(block, hir::Insn::PatchPoint { invariant: hir::Invariant::NoTracePoint, state }); + fun.push_insn(block, hir::Insn::PatchPoint { + invariant: hir::Invariant::MethodRedefined { + klass: recv_class, + method: mid, + cme: target_cme + }, state + }); + if recv_class.instance_can_have_singleton_class() { + fun.push_insn(block, hir::Insn::PatchPoint { + invariant: hir::Invariant::NoSingletonClass { klass: recv_class }, state + }); + } + Some(fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(result) })) +} diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index eca0e3a598865b..bccd27fc39dcbe 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1649,7 +1649,7 @@ impl Function { } /// Check if the type of `insn` is a subtype of `ty`. - fn is_a(&self, insn: InsnId, ty: Type) -> bool { + pub fn is_a(&self, insn: InsnId, ty: Type) -> bool { self.type_of(insn).is_subtype(ty) } @@ -2453,6 +2453,7 @@ impl Function { if let Some(profiled_type) = profiled_type { // Guard receiver class recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); + fun.insn_types[recv.0] = fun.infer_type(recv); } let cfunc = unsafe { get_mct_func(cfunc) }.cast(); @@ -13447,4 +13448,342 @@ mod opt_tests { Return v19 "); } + + #[test] + fn test_optimize_respond_to_p_true() { + eval(r#" + class C + def foo; end + end + def test(o) = o.respond_to?(:foo) + test(C.new) + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:5: + 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:StaticSymbol[:foo] = Const Value(VALUE(0x1000)) + PatchPoint MethodRedefined(C@0x1008, respond_to?@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(C@0x1008) + v24:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] + PatchPoint MethodRedefined(C@0x1008, foo@0x1040, cme:0x1048) + PatchPoint NoSingletonClass(C@0x1008) + v28:TrueClass = Const Value(true) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v28 + "); + } + + #[test] + fn test_optimize_respond_to_p_false_no_method() { + eval(r#" + class C + end + def test(o) = o.respond_to?(:foo) + test(C.new) + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:4: + 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:StaticSymbol[:foo] = Const Value(VALUE(0x1000)) + PatchPoint MethodRedefined(C@0x1008, respond_to?@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(C@0x1008) + v24:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] + PatchPoint MethodRedefined(C@0x1008, respond_to_missing?@0x1040, cme:0x1048) + PatchPoint MethodRedefined(C@0x1008, foo@0x1070, cme:0x1078) + PatchPoint NoSingletonClass(C@0x1008) + v30:FalseClass = Const Value(false) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v30 + "); + } + + #[test] + fn test_optimize_respond_to_p_false_default_private() { + eval(r#" + class C + private + def foo; end + end + def test(o) = o.respond_to?(:foo) + test(C.new) + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:6: + 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:StaticSymbol[:foo] = Const Value(VALUE(0x1000)) + PatchPoint MethodRedefined(C@0x1008, respond_to?@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(C@0x1008) + v24:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] + PatchPoint MethodRedefined(C@0x1008, foo@0x1040, cme:0x1048) + PatchPoint NoSingletonClass(C@0x1008) + v28:FalseClass = Const Value(false) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v28 + "); + } + + #[test] + fn test_optimize_respond_to_p_false_private() { + eval(r#" + class C + private + def foo; end + end + def test(o) = o.respond_to?(:foo, false) + test(C.new) + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:6: + 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:StaticSymbol[:foo] = Const Value(VALUE(0x1000)) + v14:FalseClass = Const Value(false) + PatchPoint MethodRedefined(C@0x1008, respond_to?@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(C@0x1008) + v25:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] + PatchPoint MethodRedefined(C@0x1008, foo@0x1040, cme:0x1048) + PatchPoint NoSingletonClass(C@0x1008) + v29:FalseClass = Const Value(false) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v29 + "); + } + + #[test] + fn test_optimize_respond_to_p_falsy_private() { + eval(r#" + class C + private + def foo; end + end + def test(o) = o.respond_to?(:foo, nil) + test(C.new) + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:6: + 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:StaticSymbol[:foo] = Const Value(VALUE(0x1000)) + v14:NilClass = Const Value(nil) + PatchPoint MethodRedefined(C@0x1008, respond_to?@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(C@0x1008) + v25:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] + PatchPoint MethodRedefined(C@0x1008, foo@0x1040, cme:0x1048) + PatchPoint NoSingletonClass(C@0x1008) + v29:FalseClass = Const Value(false) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v29 + "); + } + + #[test] + fn test_optimize_respond_to_p_true_private() { + eval(r#" + class C + private + def foo; end + end + def test(o) = o.respond_to?(:foo, true) + test(C.new) + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:6: + 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:StaticSymbol[:foo] = Const Value(VALUE(0x1000)) + v14:TrueClass = Const Value(true) + PatchPoint MethodRedefined(C@0x1008, respond_to?@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(C@0x1008) + v25:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] + PatchPoint MethodRedefined(C@0x1008, foo@0x1040, cme:0x1048) + PatchPoint NoSingletonClass(C@0x1008) + v29:TrueClass = Const Value(true) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v29 + "); + } + + #[test] + fn test_optimize_respond_to_p_truthy() { + eval(r#" + class C + def foo; end + end + def test(o) = o.respond_to?(:foo, 4) + test(C.new) + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:5: + 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:StaticSymbol[:foo] = Const Value(VALUE(0x1000)) + v14:Fixnum[4] = Const Value(4) + PatchPoint MethodRedefined(C@0x1008, respond_to?@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(C@0x1008) + v25:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] + PatchPoint MethodRedefined(C@0x1008, foo@0x1040, cme:0x1048) + PatchPoint NoSingletonClass(C@0x1008) + v29:TrueClass = Const Value(true) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v29 + "); + } + + #[test] + fn test_optimize_respond_to_p_falsy() { + eval(r#" + class C + def foo; end + end + def test(o) = o.respond_to?(:foo, nil) + test(C.new) + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:5: + 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:StaticSymbol[:foo] = Const Value(VALUE(0x1000)) + v14:NilClass = Const Value(nil) + PatchPoint MethodRedefined(C@0x1008, respond_to?@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(C@0x1008) + v25:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] + PatchPoint MethodRedefined(C@0x1008, foo@0x1040, cme:0x1048) + PatchPoint NoSingletonClass(C@0x1008) + v29:TrueClass = Const Value(true) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v29 + "); + } + + #[test] + fn test_optimize_respond_to_missing() { + eval(r#" + class C + end + def test(o) = o.respond_to?(:foo) + test(C.new) + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:4: + 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:StaticSymbol[:foo] = Const Value(VALUE(0x1000)) + PatchPoint MethodRedefined(C@0x1008, respond_to?@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(C@0x1008) + v24:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] + PatchPoint MethodRedefined(C@0x1008, respond_to_missing?@0x1040, cme:0x1048) + PatchPoint MethodRedefined(C@0x1008, foo@0x1070, cme:0x1078) + PatchPoint NoSingletonClass(C@0x1008) + v30:FalseClass = Const Value(false) + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v30 + "); + } + + #[test] + fn test_do_not_optimize_redefined_respond_to_missing() { + eval(r#" + class C + def respond_to_missing?(method, include_private = false) + true + end + end + def test(o) = o.respond_to?(:foo) + test(C.new) + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:7: + 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:StaticSymbol[:foo] = Const Value(VALUE(0x1000)) + PatchPoint MethodRedefined(C@0x1008, respond_to?@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(C@0x1008) + v24:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] + v25:BasicObject = CCallVariadic respond_to?@0x1040, v24, v13 + CheckInterrupts + Return v25 + "); + } } From 9b2216954a34934fd855deb642a4369fc009c68a Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Fri, 17 Oct 2025 19:40:58 -0500 Subject: [PATCH 6/7] [DOC] Tweaks for String#rindex --- doc/string/index.rdoc | 2 +- doc/string/rindex.rdoc | 52 +++++++++++++++++++++++++++++++++++++++++ string.c | 53 ++---------------------------------------- 3 files changed, 55 insertions(+), 52 deletions(-) create mode 100644 doc/string/rindex.rdoc diff --git a/doc/string/index.rdoc b/doc/string/index.rdoc index cc34bc68e6b6bd..6045fac0f6fb5a 100644 --- a/doc/string/index.rdoc +++ b/doc/string/index.rdoc @@ -11,7 +11,7 @@ returns the index of the first matching substring in +self+: 'тест'.index('с') # => 2 # Characters, not bytes. 'こんにちは'.index('ち') # => 3 -When +pattern is a Regexp, returns the index of the first match in +self+: +When +pattern+ is a Regexp, returns the index of the first match in +self+: 'foo'.index(/o./) # => 1 'foo'.index(/.o/) # => 0 diff --git a/doc/string/rindex.rdoc b/doc/string/rindex.rdoc new file mode 100644 index 00000000000000..8a1cc0106f59fb --- /dev/null +++ b/doc/string/rindex.rdoc @@ -0,0 +1,52 @@ +Returns the integer position of the _last_ substring that matches the given argument +pattern+, +or +nil+ if none found. + +When +pattern+ is a string, returns the index of the last matching substring in self: + + 'foo'.rindex('f') # => 0 + 'foo'.rindex('o') # => 2 + 'foo'.rindex('oo' # => 1 + 'foo'.rindex('ooo') # => nil + 'тест'.rindex('т') # => 3 + 'こんにちは'.rindex('ち') # => 3 + +When +pattern+ is a Regexp, returns the index of the last match in self: + + 'foo'.rindex(/f/) # => 0 + 'foo'.rindex(/o/) # => 2 + 'foo'.rindex(/oo/) # => 1 + 'foo'.rindex(/ooo/) # => nil + +When +offset+ is non-negative, it specifies the maximum starting position in the +string to end the search: + + 'foo'.rindex('o', 0) # => nil + 'foo'.rindex('o', 1) # => 1 + 'foo'.rindex('o', 2) # => 2 + 'foo'.rindex('o', 3) # => 2 + +With negative integer argument +offset+, +selects the search position by counting backward from the end of +self+: + + 'foo'.rindex('o', -1) # => 2 + 'foo'.rindex('o', -2) # => 1 + 'foo'.rindex('o', -3) # => nil + 'foo'.rindex('o', -4) # => nil + +The last match means starting at the possible last position, not +the last of longest matches: + + 'foo'.rindex(/o+/) # => 2 + $~ # => # + +To get the last longest match, combine with negative lookbehind: + + 'foo'.rindex(/(? 1 + $~ # => # + +Or String#index with negative lookforward. + + 'foo'.index(/o+(?!.*o)/) # => 1 + $~ # => # + +Related: see {Querying}[rdoc-ref:String@Querying]. diff --git a/string.c b/string.c index fab1509bac0ead..47de66eca6f3df 100644 --- a/string.c +++ b/string.c @@ -4770,59 +4770,10 @@ rb_str_rindex(VALUE str, VALUE sub, long pos) /* * call-seq: - * rindex(substring, offset = self.length) -> integer or nil - * rindex(regexp, offset = self.length) -> integer or nil + * rindex(pattern, offset = self.length) -> integer or nil * - * Returns the Integer index of the _last_ occurrence of the given +substring+, - * or +nil+ if none found: + * :include:doc/string/rindex.rdoc * - * 'foo'.rindex('f') # => 0 - * 'foo'.rindex('o') # => 2 - * 'foo'.rindex('oo') # => 1 - * 'foo'.rindex('ooo') # => nil - * - * Returns the Integer index of the _last_ match for the given Regexp +regexp+, - * or +nil+ if none found: - * - * 'foo'.rindex(/f/) # => 0 - * 'foo'.rindex(/o/) # => 2 - * 'foo'.rindex(/oo/) # => 1 - * 'foo'.rindex(/ooo/) # => nil - * - * The _last_ match means starting at the possible last position, not - * the last of longest matches. - * - * 'foo'.rindex(/o+/) # => 2 - * $~ #=> # - * - * To get the last longest match, needs to combine with negative - * lookbehind. - * - * 'foo'.rindex(/(? 1 - * $~ #=> # - * - * Or String#index with negative lookforward. - * - * 'foo'.index(/o+(?!.*o)/) # => 1 - * $~ #=> # - * - * Integer argument +offset+, if given and non-negative, specifies the maximum starting position in the - * string to _end_ the search: - * - * 'foo'.rindex('o', 0) # => nil - * 'foo'.rindex('o', 1) # => 1 - * 'foo'.rindex('o', 2) # => 2 - * 'foo'.rindex('o', 3) # => 2 - * - * If +offset+ is a negative Integer, the maximum starting position in the - * string to _end_ the search is the sum of the string's length and +offset+: - * - * 'foo'.rindex('o', -1) # => 2 - * 'foo'.rindex('o', -2) # => 1 - * 'foo'.rindex('o', -3) # => nil - * 'foo'.rindex('o', -4) # => nil - * - * Related: String#index. */ static VALUE From 7989a2ff46e0dc8dc26b1571215768801fa04463 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 17 Oct 2025 17:28:55 -0400 Subject: [PATCH 7/7] Preallocate capacity for id table in rb_singleton_class_clone_and_attach We know the exact capacity for the constant table created in rb_singleton_class_clone_and_attach so we can preallocate it. --- class.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/class.c b/class.c index c8bf624140d726..77f2fba51647bc 100644 --- a/class.c +++ b/class.c @@ -1193,7 +1193,7 @@ rb_singleton_class_clone_and_attach(VALUE obj, VALUE attach) if (RCLASS_CONST_TBL(klass)) { struct clone_const_arg arg; struct rb_id_table *table; - arg.tbl = table = rb_id_table_create(0); + arg.tbl = table = rb_id_table_create(rb_id_table_size(RCLASS_CONST_TBL(klass))); arg.klass = clone; rb_id_table_foreach(RCLASS_CONST_TBL(klass), clone_const_i, &arg); RCLASS_SET_CONST_TBL(clone, table, false);