Skip to content

Commit cccfc17

Browse files
committed
conditionally call initialize
1 parent 52a8c02 commit cccfc17

7 files changed

Lines changed: 151 additions & 56 deletions

File tree

compile.c

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9656,20 +9656,29 @@ compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, co
96569656
LABEL *not_basic_new = NEW_LABEL(nd_line(node));
96579657
LABEL *not_basic_new_finish = NEW_LABEL(nd_line(node));
96589658

9659-
// Detect String.new and String.new(..., capacity: <n>): emit opt_string_new
9660-
// so the allocation can be pre-sized. The instruction guards at runtime that
9661-
// the receiver is String and "new" is unredefined, so a compile-time name
9662-
// match is sufficient here. string_new_capa_loc is the TOPN index of the
9663-
// capacity argument, or -1 when not applicable. When the only arguments are
9664-
// ones the allocation already satisfies (nothing, or just capacity),
9665-
// initialize is a no-op and string_new_skip_init lets us omit the call.
9659+
// Detect String.new with no positional arguments: allocate via the
9660+
// String-specific opt_string_new instead of the generic opt_new. The
9661+
// instruction guards at runtime that the receiver is String and "new" is
9662+
// unredefined, so a compile-time name match is sufficient here.
9663+
//
9664+
// - string_new_capa_loc is the TOPN offset of the capacity: argument, or
9665+
// -1 when none is given. It can be deeper than 0 because other keywords
9666+
// (e.g. encoding:) may sit above it on the stack.
9667+
// - string_new_skip_init means the allocation already satisfies the call
9668+
// (nothing, or just capacity), so #initialize is omitted. Otherwise a
9669+
// trailing initialize send applies the remaining keywords, like opt_new.
96669670
int string_new_capa_loc = -1;
9671+
bool string_new = false;
96679672
bool string_new_skip_init = false;
96689673
if (inline_new &&
96699674
!(flag & (VM_CALL_ARGS_SPLAT | VM_CALL_KW_SPLAT | VM_CALL_FORWARDING))) {
96709675
const NODE *recv_node = get_nd_recv(node);
9676+
// NUM2INT(argc) is the positional count (new_callinfo adds the keywords
9677+
// separately), so it must be zero for both the bare and capacity forms.
96719678
if (recv_node && nd_type_p(recv_node, NODE_CONST) &&
9672-
RNODE_CONST(recv_node)->nd_vid == rb_intern("String")) {
9679+
RNODE_CONST(recv_node)->nd_vid == rb_intern("String") &&
9680+
NUM2INT(argc) == 0) {
9681+
string_new = true;
96739682
int keyword_len = keywords ? keywords->keyword_len : 0;
96749683
if (keywords) {
96759684
VALUE capacity_sym = ID2SYM(rb_intern("capacity"));
@@ -9680,11 +9689,7 @@ compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, co
96809689
}
96819690
}
96829691
}
9683-
// NUM2INT(argc) is the positional count (new_callinfo adds the
9684-
// keywords separately). Skip initialize when there are no
9685-
// positional args and either no keywords or only capacity.
9686-
if (NUM2INT(argc) == 0 &&
9687-
(keyword_len == 0 || (keyword_len == 1 && string_new_capa_loc >= 0))) {
9692+
if (keyword_len == 0 || (keyword_len == 1 && string_new_capa_loc >= 0)) {
96889693
string_new_skip_init = true;
96899694
}
96909695
}
@@ -9699,10 +9704,11 @@ compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, co
96999704
else {
97009705
ci = (VALUE)new_callinfo(iseq, mid, NUM2INT(argc), flag, keywords, 0);
97019706
}
9702-
if (string_new_skip_init) {
9703-
// opt_string_new allocates the String and skips initialize entirely.
9704-
// capa_loc is unused for the bare form (argc == 0).
9705-
ADD_INSN3(ret, node, opt_string_new, ci, not_basic_new, INT2FIX(string_new_capa_loc >= 0 ? string_new_capa_loc : 0));
9707+
if (string_new) {
9708+
// opt_string_new allocates the String via the String-specific
9709+
// allocator (pre-sized when capacity: is present). Whether
9710+
// #initialize runs is decided by the trailing bytecode below.
9711+
ADD_INSN3(ret, node, opt_string_new, ci, not_basic_new, INT2FIX(string_new_capa_loc));
97069712
}
97079713
else {
97089714
ADD_INSN2(ret, node, opt_new, ci, not_basic_new);

insns.def

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -967,17 +967,20 @@ opt_string_new
967967
vm_method_cfunc_is(GET_CFP(), cd, recv, rb_class_new_instance_pass_kw) &&
968968
vm_method_basic_definition_p(recv, idInitialize)) {
969969

970-
if (argc == 0) {
970+
if (capa_loc == (rb_num_t)-1) {
971+
// No capacity given: allocate an empty (embedded) buffer. When a
972+
// trailing #initialize send follows (e.g. String.new(encoding:)) it
973+
// applies the remaining arguments; otherwise this is the result.
971974
str = rb_str_buf_new(0);
972975
}
973976
else {
977+
// The capacity is not necessarily the top of the stack: with
978+
// String.new(capacity:, encoding:) the encoding sits above it.
974979
VALUE rbcapa = TOPN(capa_loc);
975980
if (FIXNUM_P(rbcapa)) {
976-
long capa = FIX2LONG(rbcapa);
977-
if (capa < 0) {
978-
capa = 0;
979-
}
980-
str = rb_str_new_capa_for_init(capa);
981+
// rb_str_new_capa_for_init floors anything below the minimum
982+
// buffer size, including negatives, so no clamping is needed.
983+
str = rb_str_new_capa_for_init(FIX2LONG(rbcapa));
981984
}
982985
}
983986

prism_compile.c

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3877,21 +3877,31 @@ pm_compile_call(rb_iseq_t *iseq, const pm_call_node_t *call_node, LINK_ANCHOR *c
38773877
call_node->block == NULL &&
38783878
(flags & VM_CALL_ARGS_BLOCKARG) == 0;
38793879

3880-
// Detect String.new and String.new(..., capacity: <n>): emit opt_string_new
3881-
// so the allocation can be pre-sized. The instruction guards at runtime that
3882-
// the receiver is String and "new" is unredefined, so a compile-time name
3883-
// match is sufficient here. string_new_capa_loc is the TOPN index of the
3884-
// capacity argument, or -1 when not applicable. When the only arguments are
3885-
// ones the allocation already satisfies (nothing, or just capacity),
3886-
// initialize is a no-op and string_new_skip_init lets us omit the call.
3880+
// Detect String.new with no positional arguments: allocate via the
3881+
// String-specific opt_string_new instead of the generic opt_new. The
3882+
// instruction guards at runtime that the receiver is String and "new" is
3883+
// unredefined, so a compile-time name match is sufficient here.
3884+
//
3885+
// - string_new_capa_loc is the TOPN offset of the capacity: argument, or
3886+
// -1 when none is given. It can be deeper than 0 because other keywords
3887+
// (e.g. encoding:) may sit above it on the stack.
3888+
// - string_new_skip_init means the allocation already satisfies the call
3889+
// (nothing, or just capacity), so #initialize is omitted. Otherwise a
3890+
// trailing initialize send applies the remaining keywords, exactly like
3891+
// opt_new does.
38873892
int string_new_capa_loc = -1;
3893+
bool string_new = false;
38883894
bool string_new_skip_init = false;
38893895
if (inline_new &&
38903896
!(flags & (VM_CALL_ARGS_SPLAT | VM_CALL_KW_SPLAT | VM_CALL_FORWARDING)) &&
38913897
call_node->receiver != NULL &&
38923898
PM_NODE_TYPE_P(call_node->receiver, PM_CONSTANT_READ_NODE)) {
38933899
const pm_constant_read_node_t *recv = (const pm_constant_read_node_t *) call_node->receiver;
3894-
if (pm_constant_id_lookup(scope_node, recv->name) == rb_intern("String")) {
3900+
// orig_argc is the positional count (new_callinfo adds the keywords
3901+
// separately).
3902+
if (pm_constant_id_lookup(scope_node, recv->name) == rb_intern("String") &&
3903+
orig_argc == 0) {
3904+
string_new = true;
38953905
int keyword_len = kw_arg ? kw_arg->keyword_len : 0;
38963906
if (kw_arg) {
38973907
VALUE capacity_sym = ID2SYM(rb_intern("capacity"));
@@ -3902,11 +3912,7 @@ pm_compile_call(rb_iseq_t *iseq, const pm_call_node_t *call_node, LINK_ANCHOR *c
39023912
}
39033913
}
39043914
}
3905-
// orig_argc is the positional count (new_callinfo adds the keywords
3906-
// separately). Skip initialize when there are no positional args and
3907-
// either no keywords or only capacity.
3908-
if (orig_argc == 0 &&
3909-
(keyword_len == 0 || (keyword_len == 1 && string_new_capa_loc >= 0))) {
3915+
if (keyword_len == 0 || (keyword_len == 1 && string_new_capa_loc >= 0)) {
39103916
string_new_skip_init = true;
39113917
}
39123918
}
@@ -3931,10 +3937,11 @@ pm_compile_call(rb_iseq_t *iseq, const pm_call_node_t *call_node, LINK_ANCHOR *c
39313937
ci = (VALUE)new_callinfo(iseq, method_id, orig_argc, flags, kw_arg, 0);
39323938
}
39333939

3934-
if (string_new_skip_init) {
3935-
// opt_string_new allocates the String and skips initialize entirely.
3936-
// capa_loc is unused for the bare form (orig_argc == 0).
3937-
PUSH_INSN3(ret, location, opt_string_new, ci, not_basic_new, INT2FIX(string_new_capa_loc >= 0 ? string_new_capa_loc : 0));
3940+
if (string_new) {
3941+
// opt_string_new allocates the String via the String-specific
3942+
// allocator (pre-sized when capacity: is present). Whether
3943+
// #initialize runs is decided by the trailing bytecode below.
3944+
PUSH_INSN3(ret, location, opt_string_new, ci, not_basic_new, INT2FIX(string_new_capa_loc));
39383945
}
39393946
else {
39403947
PUSH_INSN2(ret, location, opt_new, ci, not_basic_new);

test/ruby/test_string.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,46 @@ def test_initialize
8686
end
8787
end
8888

89+
def test_initialize_keyword_encoding
90+
# These go through the opt_string_new "hybrid" path (allocate via the
91+
# String-specific allocator, then run #initialize for the encoding).
92+
s = String.new(encoding: "UTF-8")
93+
assert_equal(Encoding::UTF_8, s.encoding)
94+
assert_equal("", s)
95+
assert_not_predicate(s, :frozen?)
96+
s << "abc"
97+
assert_equal("abc", s)
98+
99+
s = String.new(capacity: 100, encoding: "UTF-8")
100+
assert_equal(Encoding::UTF_8, s.encoding)
101+
assert_equal("", s)
102+
s << "abc"
103+
assert_equal("abc", s)
104+
105+
# Reversed keyword order (capacity is the top of the stack).
106+
s = String.new(encoding: "UTF-8", capacity: 100)
107+
assert_equal(Encoding::UTF_8, s.encoding)
108+
assert_equal("", s)
109+
110+
s = String.new(encoding: Encoding::US_ASCII)
111+
assert_equal(Encoding::US_ASCII, s.encoding)
112+
end
113+
114+
def test_initialize_keyword_redefined
115+
assert_separately([], <<-'RUBY')
116+
EnvUtil.suppress_warning do
117+
class String
118+
def initialize(*args, **kw)
119+
@marked = true
120+
end
121+
end
122+
end
123+
assert_equal(true, String.new(encoding: "UTF-8").instance_variable_get(:@marked))
124+
assert_equal(true, String.new(capacity: 100, encoding: "UTF-8").instance_variable_get(:@marked))
125+
assert_equal(true, String.new.instance_variable_get(:@marked))
126+
RUBY
127+
end
128+
89129
def test_initialize_shared
90130
S(str = "mystring" * 10).__send__(:initialize, capacity: str.bytesize)
91131
assert_equal("mystring", str[0, 8])

test/ruby/test_zjit.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,36 @@ def initialize(*) = (super; @marked = true)
225225
}, insns: [:opt_string_new], call_threshold: 2
226226
end
227227

228+
def test_opt_string_new_with_encoding
229+
# Hybrid: opt_string_new allocates, then a trailing #initialize applies the encoding.
230+
assert_compiles '["UTF-8", true]', %q{
231+
def test = String.new(encoding: "UTF-8")
232+
test; test
233+
s = test
234+
[s.encoding.name, s.empty?]
235+
}, insns: [:opt_string_new], call_threshold: 2
236+
end
237+
238+
def test_opt_string_new_with_capacity_and_encoding
239+
# Hybrid with a pre-sized buffer; capacity is below encoding on the stack.
240+
assert_compiles '["UTF-8", "abc"]', %q{
241+
def test = String.new(capacity: 100, encoding: "UTF-8")
242+
test; test
243+
s = test
244+
s << "abc"
245+
[s.encoding.name, s]
246+
}, insns: [:opt_string_new], call_threshold: 2
247+
end
248+
249+
def test_opt_string_new_with_encoding_then_capacity
250+
# Keyword order reversed: capacity is the top of the stack (capa_loc 0).
251+
assert_compiles '"UTF-8"', %q{
252+
def test = String.new(encoding: "UTF-8", capacity: 100)
253+
test; test
254+
test.encoding.name
255+
}, insns: [:opt_string_new], call_threshold: 2
256+
end
257+
228258
def test_uncached_getconstant_path
229259
assert_compiles RUBY_COPYRIGHT.dump, %q{
230260
def test = RUBY_COPYRIGHT

yjit/src/codegen.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4963,7 +4963,8 @@ fn gen_opt_string_new(
49634963
) -> Option<CodegenStatus> {
49644964
let cd = jit.get_arg(0).as_ptr();
49654965
let jump_offset = jit.get_arg(1).as_i32();
4966-
let capa_loc = jit.get_arg(2).as_i32();
4966+
// TOPN offset of the capacity: argument, or -1 when none was given.
4967+
let capa_loc = jit.get_arg(2).as_i64();
49674968

49684969
if !jit.at_compile_target() {
49694970
return jit.defer_compilation(asm);
@@ -5004,22 +5005,26 @@ fn gen_opt_string_new(
50045005
if jit.assume_expected_cfunc(asm, comptime_recv_klass, mid, rb_class_new_instance_pass_kw as _)
50055006
&& assume_method_basic_definition(jit, asm, comptime_recv_klass, ID!(initialize)) {
50065007

5007-
let str = if argc == 0 {
5008-
// bare String.new: an empty (embedded) string. rb_str_buf_new skips
5009-
// the encoding/coderange setup that rb_str_new would do.
5008+
let str = if capa_loc == -1 {
5009+
// No capacity given: an empty (embedded) string. rb_str_buf_new skips
5010+
// the encoding/coderange setup that rb_str_new would do. When a
5011+
// trailing initialize send follows (e.g. String.new(encoding:)) it
5012+
// applies the remaining arguments.
50105013
jit_prepare_non_leaf_call(jit, asm);
50115014
asm.ccall(rb_str_buf_new as *const u8, vec![Opnd::Imm(0)])
50125015
} else {
5013-
// String.new(capacity: n): only fixnum capacities take the fast path.
5014-
// A non-fixnum side-exits so the interpreter takes the generic path.
5015-
let capa = asm.stack_opnd(capa_loc);
5016+
// The capacity is at TOPN(capa_loc) (not necessarily the top of the
5017+
// stack: with String.new(capacity:, encoding:) the encoding sits
5018+
// above it). Only fixnum capacities take the fast path; a non-fixnum
5019+
// side-exits so the interpreter takes the generic path.
5020+
let capa = asm.stack_opnd(capa_loc as i32);
50165021
guard_object_is_fixnum(jit, asm, capa, capa.into());
50175022

50185023
jit_prepare_non_leaf_call(jit, asm);
50195024

50205025
// FIX2LONG; rb_str_new_capa_for_init floors anything below the
50215026
// minimum buffer size (including negatives) so no clamping needed.
5022-
let capa = asm.load(asm.stack_opnd(capa_loc));
5027+
let capa = asm.load(asm.stack_opnd(capa_loc as i32));
50235028
let capa = asm.rshift(capa, Opnd::UImm(1));
50245029
asm.ccall(rb_str_new_capa_for_init as *const u8, vec![capa])
50255030
};

zjit/src/hir.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8088,7 +8088,8 @@ fn add_iseq_to_hir(
80888088
// mirroring the interpreter's opt_string_new fast path.
80898089
let cd: *const rb_call_data = get_arg(pc, 0).as_ptr();
80908090
let dst = get_arg(pc, 1).as_i64();
8091-
let capa_loc = get_arg(pc, 2).as_u64() as usize;
8091+
// TOPN offset of the capacity: argument, or -1 when none was given.
8092+
let capa_loc = get_arg(pc, 2).as_i64();
80928093

80938094
let argc = crate::profile::num_arguments_on_stack(cd);
80948095
let ci = unsafe { get_call_data_ci(cd) };
@@ -8140,16 +8141,19 @@ fn add_iseq_to_hir(
81408141
state: exit_id,
81418142
});
81428143

8143-
let str = if argc == 0 {
8144-
// Bare String.new: rb_str_buf_new(0) yields an embedded empty string,
8145-
// skipping the encoding/coderange setup rb_str_new would do.
8144+
let str = if capa_loc == -1 {
8145+
// No capacity given: rb_str_buf_new(0) yields an embedded empty string,
8146+
// skipping the encoding/coderange setup rb_str_new would do. A trailing
8147+
// initialize send (e.g. String.new(encoding:)) applies the rest.
81468148
let capa = fun.push_insn(block, Insn::Const { val: Const::CInt64(0) });
81478149
fun.push_insn(block, Insn::StringNew { capa, cfunc: rb_str_buf_new as *const u8, state: exit_id })
81488150
} else {
8149-
// String.new(capacity: n): only the Fixnum case is handled here; anything
8150-
// else side-exits and the interpreter re-runs opt_string_new (which jumps
8151-
// to the generic path for a non-Fixnum capacity).
8152-
let capa_val = state.stack_topn(capa_loc)?;
8151+
// The capacity is at TOPN(capa_loc), not necessarily the top of the stack
8152+
// (with String.new(capacity:, encoding:) the encoding sits above it). Only
8153+
// the Fixnum case is handled here; anything else side-exits and the
8154+
// interpreter re-runs opt_string_new (which jumps to the generic path for
8155+
// a non-Fixnum capacity).
8156+
let capa_val = state.stack_topn(capa_loc as usize)?;
81538157
let capa_fix = fun.push_insn(block, Insn::GuardType { val: capa_val, guard_type: types::Fixnum, state: exit_id, recompile: None });
81548158
let capa = fun.push_insn(block, Insn::UnboxFixnum { val: capa_fix });
81558159
fun.push_insn(block, Insn::StringNew { capa, cfunc: rb_str_new_capa_for_init as *const u8, state: exit_id })

0 commit comments

Comments
 (0)