diff --git a/insns.def b/insns.def index 4a7b0a3ca066c4..35afe28a1a5905 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/string.c b/string.c index 01cfc9c17563cc..20873a35a5579a 100644 --- a/string.c +++ b/string.c @@ -10361,10 +10361,12 @@ lstrip_offset(VALUE str, const char *s, const char *e, rb_encoding *enc) * call-seq: * lstrip! -> self or nil * - * Like String#lstrip, except that any modifications are made in +self+; - * returns +self+ if any modification are made, +nil+ otherwise. + * Like String#lstrip, except that: + * + * - Performs stripping in +self+ (not in a copy of +self+). + * - Returns +self+ if any characters are stripped, +nil+ otherwise. * - * Related: String#rstrip!, String#strip!. + * Related: see {Modifying}[rdoc-ref:String@Modifying]. */ static VALUE diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 4d8eb30c804cd5..1d80093d57d47a 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -308,6 +308,15 @@ def test_setlocal_on_eval } end + def test_getblockparamproxy + assert_compiles '1', %q{ + def test(&block) + 0.then(&block) + end + test { 1 } + }, insns: [:getblockparamproxy] + end + def test_call_a_forwardable_method assert_runs '[]', %q{ def test_root = forwardable @@ -2088,6 +2097,61 @@ def test(str) } 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/vm_core.h b/vm_core.h index 9986d3e9231163..1d02298c6cd439 100644 --- a/vm_core.h +++ b/vm_core.h @@ -261,10 +261,8 @@ union ic_serial_entry { struct iseq_inline_constant_cache_entry { VALUE flags; - VALUE value; // v0 - VALUE _unused1; // v1 - VALUE _unused2; // v2 - const rb_cref_t *ic_cref; // v3 + VALUE value; + const rb_cref_t *ic_cref; }; STATIC_ASSERT(sizeof_iseq_inline_constant_cache_entry, (offsetof(struct iseq_inline_constant_cache_entry, ic_cref) + diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 4d1ce5df5eb746..a681d479770948 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -502,8 +502,6 @@ pub type ruby_vm_throw_flags = u32; pub struct iseq_inline_constant_cache_entry { pub flags: VALUE, pub value: VALUE, - pub _unused1: VALUE, - pub _unused2: VALUE, pub ic_cref: *const rb_cref_t, } #[repr(C)] diff --git a/zjit.rb b/zjit.rb index 39b0e5eb221af6..5e647bfbeeeb89 100644 --- a/zjit.rb +++ b/zjit.rb @@ -101,7 +101,7 @@ def print_counters_with_prefix(buf:, stats:, prefix:, prompt:, limit: nil) counters = stats.select { |key, value| key.start_with?(prefix) && value > 0 } return if stats.empty? - counters.transform_keys! { |key| key.to_s.delete_prefix!(prefix) } + counters.transform_keys! { |key| key.to_s.delete_prefix(prefix) } left_pad = counters.keys.map(&:size).max right_pad = counters.values.map { |value| number_with_delimiter(value).size }.max total = counters.values.sum diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 1db1ddc510c0dc..ca25084d950999 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)), @@ -397,6 +398,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::GetGlobal { id, state } => gen_getglobal(jit, asm, *id, &function.frame_state(*state)), &Insn::GetLocal { ep_offset, level } => gen_getlocal_with_ep(asm, ep_offset, level), &Insn::SetLocal { val, ep_offset, level } => no_output!(gen_setlocal_with_ep(asm, opnd!(val), function.type_of(val), ep_offset, level)), + &Insn::GetBlockParamProxy { level, state } => gen_get_block_param_proxy(jit, asm, level, &function.frame_state(state)), Insn::GetConstantPath { ic, state } => gen_get_constant_path(jit, asm, *ic, &function.frame_state(*state)), Insn::SetIvar { self_val, id, val, state: _ } => no_output!(gen_setivar(asm, opnd!(self_val), *id, opnd!(val))), Insn::SideExit { state, reason } => no_output!(gen_side_exit(jit, asm, reason, &function.frame_state(*state))), @@ -547,6 +549,29 @@ fn gen_setlocal_with_ep(asm: &mut Assembler, val: Opnd, val_type: Type, local_ep } } +fn gen_get_block_param_proxy(jit: &JITState, asm: &mut Assembler, level: u32, state: &FrameState) -> lir::Opnd { + // Bail out if the `&block` local variable has been modified + let ep = gen_get_ep(asm, level); + let flags = Opnd::mem(64, ep, SIZEOF_VALUE_I32 * (VM_ENV_DATA_INDEX_FLAGS as i32)); + asm.test(flags, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM.into()); + asm.jnz(side_exit(jit, state, SideExitReason::BlockParamProxyModified)); + + // This handles two cases which are nearly identical + // Block handler is a tagged pointer. Look at the tag. + // VM_BH_ISEQ_BLOCK_P(): block_handler & 0x03 == 0x01 + // VM_BH_IFUNC_P(): block_handler & 0x03 == 0x03 + // So to check for either of those cases we can use: val & 0x1 == 0x1 + const _: () = assert!(RUBY_SYMBOL_FLAG & 1 == 0, "guard below rejects symbol block handlers"); + + // Bail ouf if the block handler is neither ISEQ nor ifunc + let block_handler = asm.load(Opnd::mem(64, ep, SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL)); + asm.test(block_handler, 0x1.into()); + asm.jz(side_exit(jit, state, SideExitReason::BlockParamProxyNotIseqOrIfunc)); + + // Return the rb_block_param_proxy instance (GC root, so put as a number to avoid unnecessary GC tracing) + unsafe { rb_block_param_proxy }.as_u64().into() +} + fn gen_get_constant_path(jit: &JITState, asm: &mut Assembler, ic: *const iseq_inline_constant_cache, state: &FrameState) -> Opnd { unsafe extern "C" { fn rb_vm_opt_getconstant_path(ec: EcPtr, cfp: CfpPtr, ic: *const iseq_inline_constant_cache) -> VALUE; @@ -1375,6 +1400,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 +1432,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)); @@ -1570,12 +1647,12 @@ fn compile_iseq(iseq: IseqPtr) -> Result { } /// Build a Target::SideExit for non-PatchPoint instructions -fn side_exit(jit: &mut JITState, state: &FrameState, reason: SideExitReason) -> Target { +fn side_exit(jit: &JITState, state: &FrameState, reason: SideExitReason) -> Target { build_side_exit(jit, state, reason, None) } /// Build a Target::SideExit out of a FrameState -fn build_side_exit(jit: &mut JITState, state: &FrameState, reason: SideExitReason, label: Option