Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Inline Class#new. #13080

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -9380,6 +9380,7 @@ compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, co

INIT_ANCHOR(recv);
INIT_ANCHOR(args);

#if OPT_SUPPORT_JOKE
if (nd_type_p(node, NODE_VCALL)) {
ID id_bitblt;
Expand Down Expand Up @@ -9475,6 +9476,17 @@ compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, co
}

ADD_SEQ(ret, recv);

bool inline_new = ISEQ_COMPILE_DATA(iseq)->option->specialized_instruction &&
mid == rb_intern("new") &&
parent_block == NULL &&
!(flag & VM_CALL_ARGS_BLOCKARG);

if (inline_new) {
ADD_INSN(ret, node, putnil);
ADD_INSN(ret, node, swap);
}

ADD_SEQ(ret, args);

debugp_param("call args argc", argc);
Expand All @@ -9491,7 +9503,36 @@ compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, co
if ((flag & VM_CALL_ARGS_BLOCKARG) && (flag & VM_CALL_KW_SPLAT) && !(flag & VM_CALL_KW_SPLAT_MUT)) {
ADD_INSN(ret, line_node, splatkw);
}
ADD_SEND_R(ret, line_node, mid, argc, parent_block, INT2FIX(flag), keywords);

LABEL *not_basic_new = NEW_LABEL(nd_line(node));
LABEL *not_basic_new_finish = NEW_LABEL(nd_line(node));

if (inline_new) {
// Jump unless the receiver uses the "basic" implementation of "new"
VALUE ci;
if (flag & VM_CALL_FORWARDING) {
ci = (VALUE)new_callinfo(iseq, mid, NUM2INT(argc) + 1, flag, keywords, 0);
}
else {
ci = (VALUE)new_callinfo(iseq, mid, NUM2INT(argc), flag, keywords, 0);
}
ADD_INSN2(ret, node, opt_new, ci, not_basic_new);
LABEL_REF(not_basic_new);

// optimized path
ADD_SEND_R(ret, line_node, rb_intern("initialize"), argc, parent_block, INT2FIX(flag | VM_CALL_FCALL), keywords);
ADD_INSNL(ret, line_node, jump, not_basic_new_finish);

ADD_LABEL(ret, not_basic_new);
// Fall back to normal send
ADD_SEND_R(ret, line_node, mid, argc, parent_block, INT2FIX(flag), keywords);
ADD_INSN(ret, line_node, swap);

ADD_LABEL(ret, not_basic_new_finish);
ADD_INSN(ret, line_node, pop);
} else {
ADD_SEND_R(ret, line_node, mid, argc, parent_block, INT2FIX(flag), keywords);
}

qcall_branch_end(iseq, ret, else_label, branches, node, line_node);
if (popped) {
Expand Down
3 changes: 3 additions & 0 deletions debug_counter.h
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,9 @@ RB_DEBUG_COUNTER(obj_imemo_callinfo)
RB_DEBUG_COUNTER(obj_imemo_callcache)
RB_DEBUG_COUNTER(obj_imemo_constcache)

RB_DEBUG_COUNTER(opt_new_hit)
RB_DEBUG_COUNTER(opt_new_miss)

/* ar_table */
RB_DEBUG_COUNTER(artable_hint_hit)
RB_DEBUG_COUNTER(artable_hint_miss)
Expand Down
24 changes: 24 additions & 0 deletions insns.def
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,30 @@ opt_send_without_block
}
}

/* Jump if "new" method has been defined by user */
DEFINE_INSN
opt_new
(CALL_DATA cd, OFFSET dst)
()
()
// attr bool leaf = false;
{
VALUE argc = vm_ci_argc(cd->ci);
VALUE val = TOPN(argc);

if (vm_method_cfunc_is(GET_ISEQ(), cd, val, rb_class_new_instance_pass_kw) && !(ruby_vm_event_flags & ISEQ_TRACE_EVENTS)) {
RB_DEBUG_COUNTER_INC(opt_new_hit);
val = rb_obj_alloc(val);
TOPN(argc) = val;
RUBY_ASSERT(TOPN(argc + 1) == Qnil);
TOPN(argc + 1) = val;
}
else {
RB_DEBUG_COUNTER_INC(opt_new_miss);
JUMP(dst);
}
}

/* Convert object to string using to_s or equivalent. */
DEFINE_INSN
objtostring
Expand Down
12 changes: 11 additions & 1 deletion lib/erb/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,17 @@ def detect_magic_comment(s, enc = nil)
return enc, frozen
end

# :stopdoc:
WARNING_UPLEVEL = Class.new {
attr_reader :c
def initialize from
@c = caller.length - from.length
end
}.new(caller(0)).c
private_constant :WARNING_UPLEVEL
# :startdoc:

def warn_invalid_trim_mode(mode, uplevel:)
warn "Invalid ERB trim mode: #{mode.inspect} (trim_mode: nil, 0, 1, 2, or String composed of '%' and/or '-', '>', '<>')", uplevel: uplevel + 1
warn "Invalid ERB trim mode: #{mode.inspect} (trim_mode: nil, 0, 1, 2, or String composed of '%' and/or '-', '>', '<>')", uplevel: uplevel + WARNING_UPLEVEL
end
end
47 changes: 46 additions & 1 deletion prism_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -3620,6 +3620,9 @@ pm_compile_call(rb_iseq_t *iseq, const pm_call_node_t *call_node, LINK_ANCHOR *c
if (message_loc->start == NULL) message_loc = &call_node->base.location;

const pm_node_location_t location = PM_LOCATION_START_LOCATION(scope_node->parser, message_loc, call_node->base.node_id);

LINK_ELEMENT *opt_new_prelude = LAST_ELEMENT(ret);

LABEL *else_label = NEW_LABEL(location.line);
LABEL *end_label = NEW_LABEL(location.line);
LABEL *retry_end_l = NEW_LABEL(location.line);
Expand Down Expand Up @@ -3714,7 +3717,49 @@ pm_compile_call(rb_iseq_t *iseq, const pm_call_node_t *call_node, LINK_ANCHOR *c
PUSH_INSN(ret, location, splatkw);
}

PUSH_SEND_R(ret, location, method_id, INT2FIX(orig_argc), block_iseq, INT2FIX(flags), kw_arg);
LABEL *not_basic_new = NEW_LABEL(location.line);
LABEL *not_basic_new_finish = NEW_LABEL(location.line);

bool inline_new = ISEQ_COMPILE_DATA(iseq)->option->specialized_instruction &&
method_id == rb_intern("new") &&
call_node->block == NULL;

if (inline_new) {
if (LAST_ELEMENT(ret) == opt_new_prelude) {
PUSH_INSN(ret, location, putnil);
PUSH_INSN(ret, location, swap);
}
else {
ELEM_INSERT_NEXT(opt_new_prelude, &new_insn_body(iseq, location.line, location.node_id, BIN(swap), 0)->link);
ELEM_INSERT_NEXT(opt_new_prelude, &new_insn_body(iseq, location.line, location.node_id, BIN(putnil), 0)->link);
}

// Jump unless the receiver uses the "basic" implementation of "new"
VALUE ci;
if (flags & VM_CALL_FORWARDING) {
ci = (VALUE)new_callinfo(iseq, method_id, orig_argc + 1, flags, kw_arg, 0);
}
else {
ci = (VALUE)new_callinfo(iseq, method_id, orig_argc, flags, kw_arg, 0);
}

PUSH_INSN2(ret, location, opt_new, ci, not_basic_new);
LABEL_REF(not_basic_new);
// optimized path
PUSH_SEND_R(ret, location, rb_intern("initialize"), INT2FIX(orig_argc), block_iseq, INT2FIX(flags | VM_CALL_FCALL), kw_arg);
PUSH_INSNL(ret, location, jump, not_basic_new_finish);

PUSH_LABEL(ret, not_basic_new);
// Fall back to normal send
PUSH_SEND_R(ret, location, method_id, INT2FIX(orig_argc), block_iseq, INT2FIX(flags), kw_arg);
PUSH_INSN(ret, location, swap);

PUSH_LABEL(ret, not_basic_new_finish);
PUSH_INSN(ret, location, pop);
}
else {
PUSH_SEND_R(ret, location, method_id, INT2FIX(orig_argc), block_iseq, INT2FIX(flags), kw_arg);
}

if (block_iseq && ISEQ_BODY(block_iseq)->catch_table) {
pm_compile_retry_end_label(iseq, ret, retry_end_l);
Expand Down
32 changes: 23 additions & 9 deletions spec/ruby/library/objectspace/trace_object_allocations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@
require 'objspace'

describe "ObjectSpace.trace_object_allocations" do
def has_class_frame?
Class.new {
attr_reader :c

def initialize
@c = caller_locations.first.label =~ /new/
end
}.new.c
end

def obj_class_path
has_class_frame? ? "Class" : nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nil seems wrong because it looks like it basically breaks the ObjectSpace.trace_object_allocations functionality, at least the allocation_class_path and allocation_method_id methods.
Though to be fair if those basically always return "Class" and :new they probably have little value.

Notably the spec no longer works on TruffleRuby because of the changed expectations due to this commit.
TruffleRuby also inlines Class#new, and has logic inside the implementation of Class#new that if allocation tracing is enabled then it records the relevant information directly on the object, while this information is available.
Maybe CRuby could do the same? Or maybe these methods should be deprecated?

Copy link
Member

@eregon eregon May 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if CRuby would return "Class" for allocation_class_path and :new for allocation_method_id if the information is not there? Then we could revert this commit and avoid changing behavior.
Would it be incorrect? In which case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eregon We could probably do that, but to be honest I think we should delete these tests (or update them not to run inside anonymous blocks). As you noted, returning Class and :new is not very helpful information. I think the current behavior is actually more helpful. AFAICT, the reason it's returning nil in this case has something to do with how the test runner is set up (I think because everything is being executed in blocks?)

For example:

require "objspace"

class Foo
  def test
    ObjectSpace.trace_object_allocations do
      o = Object.new
      p ObjectSpace.allocation_class_path(o)
      p ObjectSpace.allocation_method_id(o)
    end
  end
end

Foo.new.test

1.times do
  ObjectSpace.trace_object_allocations do
    o = Object.new
    p ObjectSpace.allocation_class_path(o)
    p ObjectSpace.allocation_method_id(o)
  end
end

Output is this:

make runruby
RUBY_ON_BUG='gdb -x ./.gdbinit -p' ./miniruby -I./lib -I. -I.ext/common  ./tool/runruby.rb --extout=.ext  -- --disable-gems  ./test.rb 
"Foo"
:test
nil
nil

The nil is odd, but there is no class or method name in that case. Also the first example is way more useful than just returning Class and :new.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, we'll refactor those specs to move the trace_object_allocations usages inside named classes and methods and test that as it's more representative of a real usage of the feature.

end

it "runs a block" do
ScratchPad.clear
ObjectSpace.trace_object_allocations do
Expand All @@ -13,7 +27,7 @@
it "records info for allocation_class_path" do
ObjectSpace.trace_object_allocations do
o = Object.new
ObjectSpace.allocation_class_path(o).should == "Class"
ObjectSpace.allocation_class_path(o).should == obj_class_path
a = [1, 2, 3]
ObjectSpace.allocation_class_path(a).should == nil
end
Expand All @@ -31,7 +45,7 @@
it "records info for allocation_method_id" do
ObjectSpace.trace_object_allocations do
o = Object.new
ObjectSpace.allocation_method_id(o).should == :new
ObjectSpace.allocation_method_id(o).should == (has_class_frame? ? :new : nil)
a = [1, 2, 3]
ObjectSpace.allocation_method_id(a).should == nil
end
Expand All @@ -58,7 +72,7 @@
it "can be cleared using trace_object_allocations_clear" do
ObjectSpace.trace_object_allocations do
o = Object.new
ObjectSpace.allocation_class_path(o).should == "Class"
ObjectSpace.allocation_class_path(o).should == obj_class_path
ObjectSpace.trace_object_allocations_clear
ObjectSpace.allocation_class_path(o).should be_nil
end
Expand All @@ -69,14 +83,14 @@
ObjectSpace.trace_object_allocations do
o = Object.new
end
ObjectSpace.allocation_class_path(o).should == "Class"
ObjectSpace.allocation_class_path(o).should == obj_class_path
end

it "can be used without a block using trace_object_allocations_start and _stop" do
ObjectSpace.trace_object_allocations_start
begin
o = Object.new
ObjectSpace.allocation_class_path(o).should == "Class"
ObjectSpace.allocation_class_path(o).should == obj_class_path
a = [1, 2, 3]
ObjectSpace.allocation_class_path(a).should == nil
ensure
Expand All @@ -91,14 +105,14 @@
ensure
ObjectSpace.trace_object_allocations_stop
end
ObjectSpace.allocation_class_path(o).should == "Class"
ObjectSpace.allocation_class_path(o).should == obj_class_path
end

it "can be nested" do
ObjectSpace.trace_object_allocations do
ObjectSpace.trace_object_allocations do
o = Object.new
ObjectSpace.allocation_class_path(o).should == "Class"
ObjectSpace.allocation_class_path(o).should == obj_class_path
end
end
end
Expand All @@ -109,7 +123,7 @@
ObjectSpace.trace_object_allocations_start
begin
o = Object.new
ObjectSpace.allocation_class_path(o).should == "Class"
ObjectSpace.allocation_class_path(o).should == obj_class_path
ensure
ObjectSpace.trace_object_allocations_stop
end
Expand All @@ -122,7 +136,7 @@
ObjectSpace.trace_object_allocations_start
begin
o = Object.new
ObjectSpace.allocation_class_path(o).should == "Class"
ObjectSpace.allocation_class_path(o).should == obj_class_path
ObjectSpace.trace_object_allocations_stop
ensure
ObjectSpace.trace_object_allocations_stop
Expand Down
6 changes: 6 additions & 0 deletions test/erb/test_erb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@ def test_explicit_trim_line_with_carriage_return
assert_equal("line\r\n" * 3, erb.result)
end

def test_safe_level_warning
assert_warning(/#{__FILE__}:#{__LINE__ + 1}/) do
@erb.new("", 1)
end
end

def test_invalid_trim_mode
pend if RUBY_ENGINE == 'truffleruby'

Expand Down
4 changes: 2 additions & 2 deletions test/objspace/test_objspace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ def test_trace_object_allocations
assert_equal(line1, ObjectSpace.allocation_sourceline(o1))
assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(o1))
assert_equal(c1, ObjectSpace.allocation_generation(o1))
assert_equal(Class.name, ObjectSpace.allocation_class_path(o1))
assert_equal(:new, ObjectSpace.allocation_method_id(o1))
assert_equal(self.class.name, ObjectSpace.allocation_class_path(o1))
assert_equal(__method__, ObjectSpace.allocation_method_id(o1))

assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(o2))
assert_equal(line2, ObjectSpace.allocation_sourceline(o2))
Expand Down
2 changes: 2 additions & 0 deletions yjit/bindgen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ fn main() {
// From internal/object.h
.allowlist_function("rb_class_allocate_instance")
.allowlist_function("rb_obj_equal")
.allowlist_function("rb_class_new_instance_pass_kw")
.allowlist_function("rb_obj_alloc")

// From gc.h and internal/gc.h
.allowlist_function("rb_obj_info")
Expand Down
65 changes: 65 additions & 0 deletions yjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4873,6 +4873,70 @@ fn gen_throw(
Some(EndBlock)
}

fn gen_opt_new(
jit: &mut JITState,
asm: &mut Assembler,
) -> Option<CodegenStatus> {
let cd = jit.get_arg(0).as_ptr();
let jump_offset = jit.get_arg(1).as_i32();

if !jit.at_compile_target() {
return jit.defer_compilation(asm);
}

let ci = unsafe { get_call_data_ci(cd) }; // info about the call site
let mid = unsafe { vm_ci_mid(ci) };
let argc: i32 = unsafe { vm_ci_argc(ci) }.try_into().unwrap();

let recv_idx = argc;
let comptime_recv = jit.peek_at_stack(&asm.ctx, recv_idx as isize);

// This is a singleton class
let comptime_recv_klass = comptime_recv.class_of();

let recv = asm.stack_opnd(recv_idx);

perf_call!("opt_new: ", jit_guard_known_klass(
jit,
asm,
comptime_recv_klass,
recv,
recv.into(),
comptime_recv,
SEND_MAX_DEPTH,
Counter::guard_send_klass_megamorphic,
));

// We now know that it's always comptime_recv_klass
if jit.assume_expected_cfunc(asm, comptime_recv_klass, mid, rb_class_new_instance_pass_kw as _) {
// Fast path
// call rb_class_alloc to actually allocate
jit_prepare_non_leaf_call(jit, asm);
let obj = asm.ccall(rb_obj_alloc as _, vec![comptime_recv.into()]);

// Get a reference to the stack location where we need to save the
// return instance.
let result = asm.stack_opnd(recv_idx + 1);
let recv = asm.stack_opnd(recv_idx);

// Replace the receiver for the upcoming initialize call
asm.ctx.set_opnd_mapping(recv.into(), TempMapping::MapToStack(Type::UnknownHeap));
asm.mov(recv, obj);

// Save the allocated object for return
asm.ctx.set_opnd_mapping(result.into(), TempMapping::MapToStack(Type::UnknownHeap));
asm.mov(result, obj);

jump_to_next_insn(jit, asm)
} else {
// general case

// Get the branch target instruction offsets
let jump_idx = jit.next_insn_idx() as i32 + jump_offset;
return end_block_with_jump(jit, asm, jump_idx as u16);
}
}

fn gen_jump(
jit: &mut JITState,
asm: &mut Assembler,
Expand Down Expand Up @@ -10699,6 +10763,7 @@ fn get_gen_fn(opcode: VALUE) -> Option<InsnGenFn> {
YARVINSN_branchnil => Some(gen_branchnil),
YARVINSN_throw => Some(gen_throw),
YARVINSN_jump => Some(gen_jump),
YARVINSN_opt_new => Some(gen_opt_new),

YARVINSN_getblockparamproxy => Some(gen_getblockparamproxy),
YARVINSN_getblockparam => Some(gen_getblockparam),
Expand Down
Loading
Loading