diff options
| author | Aiden Fox Ivey <aiden@aidenfoxivey.com> | 2025-10-22 12:07:26 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-22 16:07:26 +0000 |
| commit | 839b1fa54f9addd1418541e03fd8396b8ad992c9 (patch) | |
| tree | 5846b986844d8e034f3dac8bcb37b558af2d0b50 | |
| parent | 6047eada20d39bbe80976c31277ec7916118f78a (diff) | |
ZJIT: Specialize String#<< to StringAppend (#14861)
Fixes https://github.com/Shopify/ruby/issues/805
| -rw-r--r-- | zjit/src/codegen.rs | 6 | ||||
| -rw-r--r-- | zjit/src/cruby_methods.rs | 15 | ||||
| -rw-r--r-- | zjit/src/hir.rs | 127 |
3 files changed, 148 insertions, 0 deletions
diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 402c500c8b..50a7295bbe 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -365,6 +365,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::StringConcat { strings, state, .. } if strings.is_empty() => return Err(*state), Insn::StringConcat { strings, state } => gen_string_concat(jit, asm, opnds!(strings), &function.frame_state(*state)), &Insn::StringGetbyteFixnum { string, index } => gen_string_getbyte_fixnum(asm, opnd!(string), opnd!(index)), + Insn::StringAppend { recv, other, state } => gen_string_append(jit, asm, opnd!(recv), opnd!(other), &function.frame_state(*state)), Insn::StringIntern { val, state } => gen_intern(asm, opnd!(val), &function.frame_state(*state)), Insn::ToRegexp { opt, values, state } => gen_toregexp(jit, asm, *opt, opnds!(values), &function.frame_state(*state)), Insn::Param => unreachable!("block.insns should not have Insn::Param"), @@ -2189,6 +2190,11 @@ fn gen_string_getbyte_fixnum(asm: &mut Assembler, string: Opnd, index: Opnd) -> asm_ccall!(asm, rb_str_getbyte, string, index) } +fn gen_string_append(jit: &mut JITState, asm: &mut Assembler, string: Opnd, val: Opnd, state: &FrameState) -> Opnd { + gen_prepare_non_leaf_call(jit, asm, state); + asm_ccall!(asm, rb_str_buf_append, string, val) +} + /// Generate a JIT entry that just increments exit_compilation_failure and exits fn gen_compile_error_counter(cb: &mut CodeBlock, compile_error: &CompileError) -> Result<CodePtr, CompileError> { let mut asm = Assembler::new(); diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 656ccab781..9d3f5a756b 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -192,6 +192,7 @@ pub fn init() -> Annotations { annotate!(rb_cString, "to_s", types::StringExact); annotate!(rb_cString, "getbyte", inline_string_getbyte); annotate!(rb_cString, "empty?", types::BoolExact, no_gc, leaf, elidable); + annotate!(rb_cString, "<<", inline_string_append); 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); @@ -276,6 +277,20 @@ fn inline_string_getbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir None } +fn inline_string_append(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> { + let &[other] = args else { return None; }; + // Inline only StringExact << String, which matches original type check from + // `vm_opt_ltlt`, which checks `RB_TYPE_P(obj, T_STRING)`. + if fun.likely_a(recv, types::StringExact, state) && fun.likely_a(other, types::String, state) { + let recv = fun.coerce_to(block, recv, types::StringExact, state); + let other = fun.coerce_to(block, other, types::String, state); + let _ = fun.push_insn(block, hir::Insn::StringAppend { recv, other, state }); + Some(recv) + } else { + None + } +} + fn inline_integer_succ(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option<hir::InsnId> { if !args.is_empty() { return None; } if fun.likely_a(recv, types::Fixnum, state) { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 68ed867e4d..fbe99d40d3 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -560,6 +560,7 @@ pub enum Insn { StringConcat { strings: Vec<InsnId>, state: InsnId }, /// Call rb_str_getbyte with known-Fixnum index StringGetbyteFixnum { string: InsnId, index: InsnId }, + StringAppend { recv: InsnId, other: InsnId, state: InsnId }, /// Combine count stack values into a regexp ToRegexp { opt: usize, values: Vec<InsnId>, state: InsnId }, @@ -956,6 +957,9 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::StringGetbyteFixnum { string, index, .. } => { write!(f, "StringGetbyteFixnum {string}, {index}") } + Insn::StringAppend { recv, other, .. } => { + write!(f, "StringAppend {recv}, {other}") + } Insn::ToRegexp { values, opt, .. } => { write!(f, "ToRegexp")?; let mut prefix = " "; @@ -1518,6 +1522,7 @@ impl Function { &StringIntern { val, state } => StringIntern { val: find!(val), state: find!(state) }, &StringConcat { ref strings, state } => StringConcat { strings: find_vec!(strings), state: find!(state) }, &StringGetbyteFixnum { string, index } => StringGetbyteFixnum { string: find!(string), index: find!(index) }, + &StringAppend { recv, other, state } => StringAppend { recv: find!(recv), other: find!(other), state: find!(state) }, &ToRegexp { opt, ref values, state } => ToRegexp { opt, values: find_vec!(values), state }, &Test { val } => Test { val: find!(val) }, &IsNil { val } => IsNil { val: find!(val) }, @@ -1712,6 +1717,7 @@ impl Function { Insn::StringIntern { .. } => types::Symbol, Insn::StringConcat { .. } => types::StringExact, Insn::StringGetbyteFixnum { .. } => types::Fixnum.union(types::NilClass), + Insn::StringAppend { .. } => types::StringExact, Insn::ToRegexp { .. } => types::RegexpExact, Insn::NewArray { .. } => types::ArrayExact, Insn::ArrayDup { .. } => types::ArrayExact, @@ -2925,6 +2931,11 @@ impl Function { worklist.push_back(string); worklist.push_back(index); } + &Insn::StringAppend { recv, other, state } => { + worklist.push_back(recv); + worklist.push_back(other); + worklist.push_back(state); + } &Insn::ToRegexp { ref values, state, .. } => { worklist.extend(values); worklist.push_back(state); @@ -13920,6 +13931,122 @@ mod opt_tests { } #[test] + fn test_optimize_string_append() { + eval(r#" + def test(x, y) = x << y + test("iron", "fish") + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@<compiled>:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@5 + v3:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2, v3) + bb1(v6:BasicObject, v7:BasicObject, v8:BasicObject): + EntryPoint JIT(0) + Jump bb2(v6, v7, v8) + bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): + PatchPoint MethodRedefined(String@0x1000, <<@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(String@0x1000) + v28:StringExact = GuardType v11, StringExact + v29:String = GuardType v12, String + v30:StringExact = StringAppend v28, v29 + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v28 + "); + } + + // TODO: This should be inlined just as in the interpreter + #[test] + fn test_optimize_string_append_non_string() { + eval(r#" + def test(x, y) = x << y + test("iron", 4) + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@<compiled>:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@5 + v3:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2, v3) + bb1(v6:BasicObject, v7:BasicObject, v8:BasicObject): + EntryPoint JIT(0) + Jump bb2(v6, v7, v8) + bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): + PatchPoint MethodRedefined(String@0x1000, <<@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(String@0x1000) + v28:StringExact = GuardType v11, StringExact + v29:BasicObject = CCallWithFrame <<@0x1038, v28, v12 + CheckInterrupts + Return v29 + "); + } + + // TODO: Should be optimized, but is waiting on String#== inlining + #[test] + fn test_optimize_string_append_string_subclass() { + eval(r#" + class MyString < String + end + def test(x, y) = x << y + test("iron", MyString.new) + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@<compiled>:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@5 + v3:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2, v3) + bb1(v6:BasicObject, v7:BasicObject, v8:BasicObject): + EntryPoint JIT(0) + Jump bb2(v6, v7, v8) + bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): + PatchPoint MethodRedefined(String@0x1000, <<@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(String@0x1000) + v28:StringExact = GuardType v11, StringExact + v29:BasicObject = CCallWithFrame <<@0x1038, v28, v12 + CheckInterrupts + Return v29 + "); + } + + #[test] + fn test_do_not_optimize_string_subclass_append_string() { + eval(r#" + class MyString < String + end + def test(x, y) = x << y + test(MyString.new, "iron") + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@<compiled>:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@5 + v3:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2, v3) + bb1(v6:BasicObject, v7:BasicObject, v8:BasicObject): + EntryPoint JIT(0) + Jump bb2(v6, v7, v8) + bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): + PatchPoint MethodRedefined(MyString@0x1000, <<@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(MyString@0x1000) + v28:HeapObject[class_exact:MyString] = GuardType v11, HeapObject[class_exact:MyString] + v29:BasicObject = CCallWithFrame <<@0x1038, v28, v12 + CheckInterrupts + Return v29 + "); + } + + #[test] fn test_dont_inline_integer_succ_with_args() { eval(" def test = 4.succ 1 |
