summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--insns.def1
-rw-r--r--test/ruby/test_zjit.rb55
-rw-r--r--zjit/src/codegen.rs53
-rw-r--r--zjit/src/cruby_bindings.inc.rs35
-rw-r--r--zjit/src/hir.rs99
-rw-r--r--zjit/src/profile.rs15
-rw-r--r--zjit/src/stats.rs2
7 files changed, 237 insertions, 23 deletions
diff --git a/insns.def b/insns.def
index 4a7b0a3ca0..35afe28a1a 100644
--- a/insns.def
+++ b/insns.def
@@ -939,6 +939,7 @@ objtostring
(VALUE recv)
(VALUE val)
// attr bool leaf = false;
+// attr bool zjit_profile = true;
{
val = vm_objtostring(GET_ISEQ(), recv, cd);
diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb
index 4d8eb30c80..b5bdd0d12b 100644
--- a/test/ruby/test_zjit.rb
+++ b/test/ruby/test_zjit.rb
@@ -2088,6 +2088,61 @@ class TestZJIT < Test::Unit::TestCase
}
end
+ def test_objtostring_profiled_string_fastpath
+ assert_compiles '"foo"', %q{
+ def test(str)
+ "#{str}"
+ end
+ test('foo'); test('foo') # profile as string
+ }, call_threshold: 2
+ end
+
+ def test_objtostring_profiled_string_subclass_fastpath
+ assert_compiles '"foo"', %q{
+ class MyString < String; end
+
+ def test(str)
+ "#{str}"
+ end
+
+ foo = MyString.new("foo")
+ test(foo); test(foo) # still profiles as string
+ }, call_threshold: 2
+ end
+
+ def test_objtostring_profiled_string_fastpath_exits_on_nonstring
+ assert_compiles '"1"', %q{
+ def test(str)
+ "#{str}"
+ end
+
+ test('foo') # profile as string
+ test(1)
+ }, call_threshold: 2
+ end
+
+ def test_objtostring_profiled_nonstring_calls_to_s
+ assert_compiles '"[1, 2, 3]"', %q{
+ def test(str)
+ "#{str}"
+ end
+
+ test([1,2,3]); # profile as nonstring
+ test([1,2,3]);
+ }, call_threshold: 2
+ end
+
+ def test_objtostring_profiled_nonstring_guard_exits_when_string
+ assert_compiles '"foo"', %q{
+ def test(str)
+ "#{str}"
+ end
+
+ test([1,2,3]); # profiles as nonstring
+ test('foo');
+ }, call_threshold: 2
+ end
+
def test_string_bytesize_with_guard
assert_compiles '5', %q{
def test(str)
diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs
index 1db1ddc510..c338d8bc1f 100644
--- a/zjit/src/codegen.rs
+++ b/zjit/src/codegen.rs
@@ -389,6 +389,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
&Insn::IsMethodCfunc { val, cd, cfunc } => gen_is_method_cfunc(jit, asm, opnd!(val), cd, cfunc),
Insn::Test { val } => gen_test(asm, opnd!(val)),
Insn::GuardType { val, guard_type, state } => gen_guard_type(jit, asm, opnd!(val), *guard_type, &function.frame_state(*state)),
+ Insn::GuardTypeNot { val, guard_type, state } => gen_guard_type_not(jit, asm, opnd!(val), *guard_type, &function.frame_state(*state)),
Insn::GuardBitEquals { val, expected, state } => gen_guard_bit_equals(jit, asm, opnd!(val), *expected, &function.frame_state(*state)),
Insn::PatchPoint { invariant, state } => no_output!(gen_patch_point(jit, asm, invariant, &function.frame_state(*state))),
Insn::CCall { cfun, args, name: _, return_type: _, elidable: _ } => gen_ccall(asm, *cfun, opnds!(args)),
@@ -1375,6 +1376,26 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, guard
asm.cmp(klass, Opnd::Value(expected_class));
asm.jne(side_exit);
+ } else if guard_type.is_subtype(types::String) {
+ let side = side_exit(jit, state, GuardType(guard_type));
+
+ // Check special constant
+ asm.test(val, Opnd::UImm(RUBY_IMMEDIATE_MASK as u64));
+ asm.jnz(side.clone());
+
+ // Check false
+ asm.cmp(val, Qfalse.into());
+ asm.je(side.clone());
+
+ let val = match val {
+ Opnd::Reg(_) | Opnd::VReg { .. } => val,
+ _ => asm.load(val),
+ };
+
+ let flags = asm.load(Opnd::mem(VALUE_BITS, val, RUBY_OFFSET_RBASIC_FLAGS));
+ let tag = asm.and(flags, Opnd::UImm(RUBY_T_MASK as u64));
+ asm.cmp(tag, Opnd::UImm(RUBY_T_STRING as u64));
+ asm.jne(side);
} else if guard_type.bit_equal(types::HeapObject) {
let side_exit = side_exit(jit, state, GuardType(guard_type));
asm.cmp(val, Opnd::Value(Qfalse));
@@ -1387,6 +1408,38 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, guard
val
}
+fn gen_guard_type_not(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, guard_type: Type, state: &FrameState) -> lir::Opnd {
+ if guard_type.is_subtype(types::String) {
+ // We only exit if val *is* a String. Otherwise we fall through.
+ let cont = asm.new_label("guard_type_not_string_cont");
+ let side = side_exit(jit, state, GuardTypeNot(guard_type));
+
+ // Continue if special constant (not string)
+ asm.test(val, Opnd::UImm(RUBY_IMMEDIATE_MASK as u64));
+ asm.jnz(cont.clone());
+
+ // Continue if false (not string)
+ asm.cmp(val, Qfalse.into());
+ asm.je(cont.clone());
+
+ let val = match val {
+ Opnd::Reg(_) | Opnd::VReg { .. } => val,
+ _ => asm.load(val),
+ };
+
+ let flags = asm.load(Opnd::mem(VALUE_BITS, val, RUBY_OFFSET_RBASIC_FLAGS));
+ let tag = asm.and(flags, Opnd::UImm(RUBY_T_MASK as u64));
+ asm.cmp(tag, Opnd::UImm(RUBY_T_STRING as u64));
+ asm.je(side);
+
+ // Otherwise (non-string heap object), continue.
+ asm.write_label(cont);
+ } else {
+ unimplemented!("unsupported type: {guard_type}");
+ }
+ val
+}
+
/// Compile an identity check with a side exit
fn gen_guard_bit_equals(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, expected: VALUE, state: &FrameState) -> lir::Opnd {
asm.cmp(val, Opnd::Value(expected));
diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs
index ae394f9c60..259b4b24bd 100644
--- a/zjit/src/cruby_bindings.inc.rs
+++ b/zjit/src/cruby_bindings.inc.rs
@@ -684,23 +684,24 @@ pub const YARVINSN_trace_putobject_INT2FIX_0_: ruby_vminsn_type = 216;
pub const YARVINSN_trace_putobject_INT2FIX_1_: ruby_vminsn_type = 217;
pub const YARVINSN_zjit_getinstancevariable: ruby_vminsn_type = 218;
pub const YARVINSN_zjit_opt_send_without_block: ruby_vminsn_type = 219;
-pub const YARVINSN_zjit_opt_nil_p: ruby_vminsn_type = 220;
-pub const YARVINSN_zjit_opt_plus: ruby_vminsn_type = 221;
-pub const YARVINSN_zjit_opt_minus: ruby_vminsn_type = 222;
-pub const YARVINSN_zjit_opt_mult: ruby_vminsn_type = 223;
-pub const YARVINSN_zjit_opt_div: ruby_vminsn_type = 224;
-pub const YARVINSN_zjit_opt_mod: ruby_vminsn_type = 225;
-pub const YARVINSN_zjit_opt_eq: ruby_vminsn_type = 226;
-pub const YARVINSN_zjit_opt_neq: ruby_vminsn_type = 227;
-pub const YARVINSN_zjit_opt_lt: ruby_vminsn_type = 228;
-pub const YARVINSN_zjit_opt_le: ruby_vminsn_type = 229;
-pub const YARVINSN_zjit_opt_gt: ruby_vminsn_type = 230;
-pub const YARVINSN_zjit_opt_ge: ruby_vminsn_type = 231;
-pub const YARVINSN_zjit_opt_and: ruby_vminsn_type = 232;
-pub const YARVINSN_zjit_opt_or: ruby_vminsn_type = 233;
-pub const YARVINSN_zjit_opt_empty_p: ruby_vminsn_type = 234;
-pub const YARVINSN_zjit_opt_not: ruby_vminsn_type = 235;
-pub const VM_INSTRUCTION_SIZE: ruby_vminsn_type = 236;
+pub const YARVINSN_zjit_objtostring: ruby_vminsn_type = 220;
+pub const YARVINSN_zjit_opt_nil_p: ruby_vminsn_type = 221;
+pub const YARVINSN_zjit_opt_plus: ruby_vminsn_type = 222;
+pub const YARVINSN_zjit_opt_minus: ruby_vminsn_type = 223;
+pub const YARVINSN_zjit_opt_mult: ruby_vminsn_type = 224;
+pub const YARVINSN_zjit_opt_div: ruby_vminsn_type = 225;
+pub const YARVINSN_zjit_opt_mod: ruby_vminsn_type = 226;
+pub const YARVINSN_zjit_opt_eq: ruby_vminsn_type = 227;
+pub const YARVINSN_zjit_opt_neq: ruby_vminsn_type = 228;
+pub const YARVINSN_zjit_opt_lt: ruby_vminsn_type = 229;
+pub const YARVINSN_zjit_opt_le: ruby_vminsn_type = 230;
+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 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 6bec2ab552..a7fa96b5c8 100644
--- a/zjit/src/hir.rs
+++ b/zjit/src/hir.rs
@@ -455,6 +455,7 @@ pub enum SideExitReason {
FixnumSubOverflow,
FixnumMultOverflow,
GuardType(Type),
+ GuardTypeNot(Type),
GuardShape(ShapeId),
GuardBitEquals(VALUE),
PatchPoint(Invariant),
@@ -474,6 +475,7 @@ impl std::fmt::Display for SideExitReason {
SideExitReason::UnknownNewarraySend(VM_OPT_NEWARRAY_SEND_PACK_BUFFER) => write!(f, "UnknownNewarraySend(PACK_BUFFER)"),
SideExitReason::UnknownNewarraySend(VM_OPT_NEWARRAY_SEND_INCLUDE_P) => write!(f, "UnknownNewarraySend(INCLUDE_P)"),
SideExitReason::GuardType(guard_type) => write!(f, "GuardType({guard_type})"),
+ SideExitReason::GuardTypeNot(guard_type) => write!(f, "GuardTypeNot({guard_type})"),
SideExitReason::GuardBitEquals(value) => write!(f, "GuardBitEquals({})", value.print(&PtrPrintMap::identity())),
SideExitReason::PatchPoint(invariant) => write!(f, "PatchPoint({invariant})"),
_ => write!(f, "{self:?}"),
@@ -623,6 +625,7 @@ pub enum Insn {
/// Side-exit if val doesn't have the expected type.
GuardType { val: InsnId, guard_type: Type, state: InsnId },
+ GuardTypeNot { val: InsnId, guard_type: Type, state: InsnId },
/// Side-exit if val is not the expected VALUE.
GuardBitEquals { val: InsnId, expected: VALUE, state: InsnId },
/// Side-exit if val doesn't have the expected shape.
@@ -859,6 +862,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
Insn::FixnumAnd { left, right, .. } => { write!(f, "FixnumAnd {left}, {right}") },
Insn::FixnumOr { left, right, .. } => { write!(f, "FixnumOr {left}, {right}") },
Insn::GuardType { val, guard_type, .. } => { write!(f, "GuardType {val}, {}", guard_type.print(self.ptr_map)) },
+ Insn::GuardTypeNot { val, guard_type, .. } => { write!(f, "GuardTypeNot {val}, {}", guard_type.print(self.ptr_map)) },
Insn::GuardBitEquals { val, expected, .. } => { write!(f, "GuardBitEquals {val}, {}", expected.print(self.ptr_map)) },
&Insn::GuardShape { val, shape, .. } => { write!(f, "GuardShape {val}, {:p}", self.ptr_map.map_shape(shape)) },
Insn::PatchPoint { invariant, .. } => { write!(f, "PatchPoint {}", invariant.print(self.ptr_map)) },
@@ -1285,6 +1289,7 @@ impl Function {
&IfTrue { val, ref target } => IfTrue { val: find!(val), target: find_branch_edge!(target) },
&IfFalse { val, ref target } => IfFalse { val: find!(val), target: find_branch_edge!(target) },
&GuardType { val, guard_type, state } => GuardType { val: find!(val), guard_type, state },
+ &GuardTypeNot { val, guard_type, state } => GuardTypeNot { val: find!(val), guard_type, state },
&GuardBitEquals { val, expected, state } => GuardBitEquals { val: find!(val), expected, state },
&GuardShape { val, shape, state } => GuardShape { val: find!(val), shape, state },
&FixnumAdd { left, right, state } => FixnumAdd { left: find!(left), right: find!(right), state },
@@ -1430,6 +1435,7 @@ impl Function {
Insn::ObjectAlloc { .. } => types::HeapObject,
Insn::CCall { 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)),
Insn::GuardShape { val, .. } => self.type_of(*val),
Insn::FixnumAdd { .. } => types::Fixnum,
@@ -1546,9 +1552,10 @@ impl Function {
fn chase_insn(&self, insn: InsnId) -> InsnId {
let id = self.union_find.borrow().find_const(insn);
match self.insns[id.0] {
- Insn::GuardType { val, .. } => self.chase_insn(val),
- Insn::GuardShape { val, .. } => self.chase_insn(val),
- Insn::GuardBitEquals { val, .. } => self.chase_insn(val),
+ Insn::GuardType { val, .. }
+ | Insn::GuardTypeNot { val, .. }
+ | Insn::GuardShape { val, .. }
+ | Insn::GuardBitEquals { val, .. } => self.chase_insn(val),
_ => id,
}
}
@@ -1791,12 +1798,26 @@ impl Function {
self.insn_types[replacement.0] = self.infer_type(replacement);
self.make_equal_to(insn_id, replacement);
}
- Insn::ObjToString { val, .. } => {
+ Insn::ObjToString { val, cd, state, .. } => {
if self.is_a(val, types::String) {
// behaves differently from `SendWithoutBlock` with `mid:to_s` because ObjToString should not have a patch point for String to_s being redefined
- self.make_equal_to(insn_id, val);
+ self.make_equal_to(insn_id, val); continue;
+ }
+
+ let frame_state = self.frame_state(state);
+ let Some(recv_type) = self.profiled_type_of_at(val, frame_state.insn_idx) else {
+ self.push_insn_id(block, insn_id); continue
+ };
+
+ if recv_type.is_string() {
+ let guard = self.push_insn(block, Insn::GuardType { val: val, guard_type: types::String, state: state });
+ // Infer type so AnyToString can fold off this
+ self.insn_types[guard.0] = self.infer_type(guard);
+ self.make_equal_to(insn_id, guard);
} else {
- self.push_insn_id(block, insn_id);
+ self.push_insn(block, Insn::GuardTypeNot { val: val, guard_type: types::String, state: state});
+ let send_to_s = self.push_insn(block, Insn::SendWithoutBlock { self_val: val, cd: cd, args: vec![], state: state});
+ self.make_equal_to(insn_id, send_to_s);
}
}
Insn::AnyToString { str, .. } => {
@@ -2193,6 +2214,7 @@ impl Function {
| &Insn::StringCopy { val, state, .. }
| &Insn::ObjectAlloc { val, state }
| &Insn::GuardType { val, state, .. }
+ | &Insn::GuardTypeNot { val, state, .. }
| &Insn::GuardBitEquals { val, state, .. }
| &Insn::GuardShape { val, state, .. }
| &Insn::ToArray { val, state }
@@ -8282,6 +8304,71 @@ mod opt_tests {
}
#[test]
+ fn test_optimize_objtostring_anytostring_recv_profiled() {
+ eval("
+ def test(a)
+ \"#{a}\"
+ end
+ test('foo'); test('foo')
+ ");
+
+ assert_snapshot!(hir_string("test"), @r"
+ fn test@<compiled>:3:
+ bb0(v0:BasicObject, v1:BasicObject):
+ v5:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
+ v17:String = GuardType v1, String
+ v11:StringExact = StringConcat v5, v17
+ CheckInterrupts
+ Return v11
+ ");
+ }
+
+ #[test]
+ fn test_optimize_objtostring_anytostring_recv_profiled_string_subclass() {
+ eval("
+ class MyString < String; end
+
+ def test(a)
+ \"#{a}\"
+ end
+ foo = MyString.new('foo')
+ test(MyString.new(foo)); test(MyString.new(foo))
+ ");
+
+ assert_snapshot!(hir_string("test"), @r"
+ fn test@<compiled>:5:
+ bb0(v0:BasicObject, v1:BasicObject):
+ v5:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
+ v17:String = GuardType v1, String
+ v11:StringExact = StringConcat v5, v17
+ CheckInterrupts
+ Return v11
+ ");
+ }
+
+ #[test]
+ fn test_optimize_objtostring_profiled_nonstring_falls_back_to_send() {
+ eval("
+ def test(a)
+ \"#{a}\"
+ end
+ test([1,2,3]); test([1,2,3]) # No fast path for array
+ ");
+
+ assert_snapshot!(hir_string("test"), @r"
+ fn test@<compiled>:3:
+ bb0(v0:BasicObject, v1:BasicObject):
+ v5:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
+ v17:BasicObject = GuardTypeNot v1, String
+ v18:BasicObject = SendWithoutBlock v1, :to_s
+ v9:String = AnyToString v1, str: v18
+ v11:StringExact = StringConcat v5, v9
+ CheckInterrupts
+ Return v11
+ ");
+ }
+
+ #[test]
fn test_branchnil_nil() {
eval("
def test
diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs
index 4d33aadf2b..c53943a457 100644
--- a/zjit/src/profile.rs
+++ b/zjit/src/profile.rs
@@ -75,6 +75,7 @@ fn profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) {
YARVINSN_opt_empty_p => profile_operands(profiler, profile, 1),
YARVINSN_opt_not => profile_operands(profiler, profile, 1),
YARVINSN_getinstancevariable => profile_self(profiler, profile),
+ YARVINSN_objtostring => profile_operands(profiler, profile, 1),
YARVINSN_opt_send_without_block => {
let cd: *const rb_call_data = profiler.insn_opnd(0).as_ptr();
let argc = unsafe { vm_ci_argc((*cd).ci) };
@@ -235,6 +236,20 @@ impl ProfiledType {
self.class == unsafe { rb_cInteger } && self.flags.is_immediate()
}
+ pub fn is_string(&self) -> bool {
+ // Fast paths for immediates and exact-class
+ if self.flags.is_immediate() {
+ return false;
+ }
+
+ let string = unsafe { rb_cString };
+ if self.class == string{
+ return true;
+ }
+
+ self.class.is_subclass_of(string) == ClassRelationship::Subclass
+ }
+
pub fn is_flonum(&self) -> bool {
self.class == unsafe { rb_cFloat } && self.flags.is_immediate()
}
diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs
index adfc28b4a8..bce353ec9d 100644
--- a/zjit/src/stats.rs
+++ b/zjit/src/stats.rs
@@ -95,6 +95,7 @@ make_counters! {
exit_fixnum_sub_overflow,
exit_fixnum_mult_overflow,
exit_guard_type_failure,
+ exit_guard_type_not_failure,
exit_guard_bit_equals_failure,
exit_guard_shape_failure,
exit_patchpoint,
@@ -227,6 +228,7 @@ pub fn exit_counter_ptr(reason: crate::hir::SideExitReason) -> *mut u64 {
FixnumSubOverflow => exit_fixnum_sub_overflow,
FixnumMultOverflow => exit_fixnum_mult_overflow,
GuardType(_) => exit_guard_type_failure,
+ GuardTypeNot(_) => exit_guard_type_not_failure,
GuardBitEquals(_) => exit_guard_bit_equals_failure,
GuardShape(_) => exit_guard_shape_failure,
PatchPoint(_) => exit_patchpoint,