summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAiden Fox Ivey <aiden@aidenfoxivey.com>2025-10-22 12:07:26 -0400
committerGitHub <noreply@github.com>2025-10-22 16:07:26 +0000
commit839b1fa54f9addd1418541e03fd8396b8ad992c9 (patch)
tree5846b986844d8e034f3dac8bcb37b558af2d0b50
parent6047eada20d39bbe80976c31277ec7916118f78a (diff)
ZJIT: Specialize String#<< to StringAppend (#14861)
Fixes https://github.com/Shopify/ruby/issues/805
-rw-r--r--zjit/src/codegen.rs6
-rw-r--r--zjit/src/cruby_methods.rs15
-rw-r--r--zjit/src/hir.rs127
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