Skip to content

Commit 8a6740c

Browse files
k0kubunmaximecb
andauthored
YJIT: Lazily push a frame for specialized C funcs (#10080)
* YJIT: Lazily push a frame for specialized C funcs Co-authored-by: Maxime Chevalier-Boisvert <maxime.chevalierboisvert@shopify.com> * Fix a comment on pc_to_cfunc * Rename rb_yjit_check_pc to rb_yjit_lazy_push_frame * Rename it to jit_prepare_lazy_frame_call * Fix a typo * Optimize String#getbyte as well * Optimize String#byteslice as well --------- Co-authored-by: Maxime Chevalier-Boisvert <maxime.chevalierboisvert@shopify.com>
1 parent 50ace99 commit 8a6740c

File tree

12 files changed

+206
-10
lines changed

12 files changed

+206
-10
lines changed

bootstraptest/test_yjit.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,6 +2046,57 @@ def getbyte(s, i)
20462046
[getbyte("a", 0), getbyte("a", 1), getbyte("a", -1), getbyte("a", -2), getbyte("a", "a")]
20472047
} unless rjit_enabled? # Not yet working on RJIT
20482048

2049+
# Basic test for String#setbyte
2050+
assert_equal 'AoZ', %q{
2051+
s = "foo"
2052+
s.setbyte(0, 65)
2053+
s.setbyte(-1, 90)
2054+
s
2055+
}
2056+
2057+
# String#setbyte IndexError
2058+
assert_equal 'String#setbyte', %q{
2059+
def ccall = "".setbyte(1, 0)
2060+
begin
2061+
ccall
2062+
rescue => e
2063+
e.backtrace.first.split("'").last
2064+
end
2065+
}
2066+
2067+
# String#setbyte TypeError
2068+
assert_equal 'String#setbyte', %q{
2069+
def ccall = "".setbyte(nil, 0)
2070+
begin
2071+
ccall
2072+
rescue => e
2073+
e.backtrace.first.split("'").last
2074+
end
2075+
}
2076+
2077+
# String#setbyte FrozenError
2078+
assert_equal 'String#setbyte', %q{
2079+
def ccall = "a".freeze.setbyte(0, 0)
2080+
begin
2081+
ccall
2082+
rescue => e
2083+
e.backtrace.first.split("'").last
2084+
end
2085+
}
2086+
2087+
# non-leaf String#setbyte
2088+
assert_equal 'String#setbyte', %q{
2089+
def to_int
2090+
@caller = caller
2091+
0
2092+
end
2093+
2094+
def ccall = "a".setbyte(self, 98)
2095+
ccall
2096+
2097+
@caller.first.split("'").last
2098+
}
2099+
20492100
# non-leaf String#byteslice
20502101
assert_equal 'TypeError', %q{
20512102
def ccall = "".byteslice(nil, nil)

common.mk

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6711,6 +6711,7 @@ error.$(OBJEXT): {$(VPATH)}util.h
67116711
error.$(OBJEXT): {$(VPATH)}vm_core.h
67126712
error.$(OBJEXT): {$(VPATH)}vm_opts.h
67136713
error.$(OBJEXT): {$(VPATH)}warning.rbinc
6714+
error.$(OBJEXT): {$(VPATH)}yjit.h
67146715
eval.$(OBJEXT): $(CCAN_DIR)/check_type/check_type.h
67156716
eval.$(OBJEXT): $(CCAN_DIR)/container_of/container_of.h
67166717
eval.$(OBJEXT): $(CCAN_DIR)/list/list.h
@@ -11425,6 +11426,7 @@ object.$(OBJEXT): {$(VPATH)}vm_core.h
1142511426
object.$(OBJEXT): {$(VPATH)}vm_debug.h
1142611427
object.$(OBJEXT): {$(VPATH)}vm_opts.h
1142711428
object.$(OBJEXT): {$(VPATH)}vm_sync.h
11429+
object.$(OBJEXT): {$(VPATH)}yjit.h
1142811430
pack.$(OBJEXT): $(CCAN_DIR)/check_type/check_type.h
1142911431
pack.$(OBJEXT): $(CCAN_DIR)/container_of/container_of.h
1143011432
pack.$(OBJEXT): $(CCAN_DIR)/list/list.h

error.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#include "ruby/util.h"
4949
#include "ruby_assert.h"
5050
#include "vm_core.h"
51+
#include "yjit.h"
5152

5253
#include "builtin.h"
5354

@@ -1409,6 +1410,7 @@ rb_exc_new_cstr(VALUE etype, const char *s)
14091410
VALUE
14101411
rb_exc_new_str(VALUE etype, VALUE str)
14111412
{
1413+
rb_yjit_lazy_push_frame(GET_EC()->cfp->pc);
14121414
StringValue(str);
14131415
return rb_class_new_instance(1, &str, etype);
14141416
}
@@ -3827,6 +3829,7 @@ inspect_frozen_obj(VALUE obj, VALUE mesg, int recur)
38273829
void
38283830
rb_error_frozen_object(VALUE frozen_obj)
38293831
{
3832+
rb_yjit_lazy_push_frame(GET_EC()->cfp->pc);
38303833
VALUE debug_info;
38313834
const ID created_info = id_debug_created_info;
38323835
VALUE mesg = rb_sprintf("can't modify frozen %"PRIsVALUE": ",

object.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#include "ruby/assert.h"
4343
#include "builtin.h"
4444
#include "shape.h"
45+
#include "yjit.h"
4546

4647
/* Flags of RObject
4748
*
@@ -3238,6 +3239,7 @@ rb_to_integer_with_id_exception(VALUE val, const char *method, ID mid, int raise
32383239
VALUE v;
32393240

32403241
if (RB_INTEGER_TYPE_P(val)) return val;
3242+
rb_yjit_lazy_push_frame(GET_EC()->cfp->pc);
32413243
v = try_to_int(val, mid, raise);
32423244
if (!raise && NIL_P(v)) return Qnil;
32433245
if (!RB_INTEGER_TYPE_P(v)) {

string.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6173,7 +6173,7 @@ rb_str_getbyte(VALUE str, VALUE index)
61736173
*
61746174
* Related: String#getbyte.
61756175
*/
6176-
static VALUE
6176+
VALUE
61776177
rb_str_setbyte(VALUE str, VALUE index, VALUE value)
61786178
{
61796179
long pos = NUM2LONG(index);

vm_insnhelper.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3535,6 +3535,24 @@ vm_call_cfunc_with_frame_(rb_execution_context_t *ec, rb_control_frame_t *reg_cf
35353535
return val;
35363536
}
35373537

3538+
// Push a C method frame for a given cme. This is called when JIT code skipped
3539+
// pushing a frame but the C method reached a point where a frame is needed.
3540+
void
3541+
rb_vm_push_cfunc_frame(const rb_callable_method_entry_t *cme, int recv_idx)
3542+
{
3543+
VM_ASSERT(cme->def->type == VM_METHOD_TYPE_CFUNC);
3544+
rb_execution_context_t *ec = GET_EC();
3545+
VALUE *sp = ec->cfp->sp;
3546+
VALUE recv = *(sp - recv_idx - 1);
3547+
VALUE frame_type = VM_FRAME_MAGIC_CFUNC | VM_FRAME_FLAG_CFRAME | VM_ENV_FLAG_LOCAL;
3548+
VALUE block_handler = VM_BLOCK_HANDLER_NONE;
3549+
#if VM_CHECK_MODE > 0
3550+
// Clean up the stack canary since we're about to satisfy the "leaf or lazy push" assumption
3551+
*(GET_EC()->cfp->sp) = Qfalse;
3552+
#endif
3553+
vm_push_frame(ec, NULL, frame_type, recv, block_handler, (VALUE)cme, 0, ec->cfp->sp, 0, 0);
3554+
}
3555+
35383556
// If true, cc->call needs to include `CALLER_SETUP_ARG` (i.e. can't be skipped in fastpath)
35393557
bool
35403558
rb_splat_or_kwargs_p(const struct rb_callinfo *restrict ci)

yjit.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ void rb_yjit_before_ractor_spawn(void);
4545
void rb_yjit_constant_ic_update(const rb_iseq_t *const iseq, IC ic, unsigned insn_idx);
4646
void rb_yjit_tracing_invalidate_all(void);
4747
void rb_yjit_show_usage(int help, int highlight, unsigned int width, int columns);
48+
void rb_yjit_lazy_push_frame(const VALUE *pc);
4849

4950
#else
5051
// !USE_YJIT
@@ -66,6 +67,7 @@ static inline void rb_yjit_iseq_free(void *payload) {}
6667
static inline void rb_yjit_before_ractor_spawn(void) {}
6768
static inline void rb_yjit_constant_ic_update(const rb_iseq_t *const iseq, IC ic, unsigned insn_idx) {}
6869
static inline void rb_yjit_tracing_invalidate_all(void) {}
70+
static inline void rb_yjit_lazy_push_frame(const VALUE *pc) {}
6971

7072
#endif // #if USE_YJIT
7173

yjit.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,10 @@ def _print_stats(out: $stderr) # :nodoc:
332332
out.puts "num_throw_break: " + format_number_pct(13, stats[:num_throw_break], stats[:num_throw])
333333
out.puts "num_throw_retry: " + format_number_pct(13, stats[:num_throw_retry], stats[:num_throw])
334334
out.puts "num_throw_return: " + format_number_pct(13, stats[:num_throw_return], stats[:num_throw])
335+
out.puts "num_lazy_frame_check: " + format_number(13, stats[:num_lazy_frame_check])
336+
out.puts "num_lazy_frame_push: " + format_number_pct(13, stats[:num_lazy_frame_push], stats[:num_lazy_frame_check])
337+
out.puts "lazy_frame_count: " + format_number(13, stats[:lazy_frame_count])
338+
out.puts "lazy_frame_failure: " + format_number(13, stats[:lazy_frame_failure])
335339

336340
out.puts "iseq_stack_too_large: " + format_number(13, stats[:iseq_stack_too_large])
337341
out.puts "iseq_too_long: " + format_number(13, stats[:iseq_too_long])

yjit/src/codegen.rs

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,54 @@ fn gen_save_sp_with_offset(asm: &mut Assembler, offset: i8) {
411411
}
412412
}
413413

414+
/// Basically jit_prepare_non_leaf_call(), but this registers the current PC
415+
/// to lazily push a C method frame when it's necessary.
416+
fn jit_prepare_lazy_frame_call(
417+
jit: &mut JITState,
418+
asm: &mut Assembler,
419+
cme: *const rb_callable_method_entry_t,
420+
recv_opnd: YARVOpnd,
421+
) -> bool {
422+
// We can use this only when the receiver is on stack.
423+
let recv_idx = match recv_opnd {
424+
StackOpnd(recv_idx) => recv_idx,
425+
_ => unreachable!("recv_opnd must be on stack, but got: {:?}", recv_opnd),
426+
};
427+
428+
// Get the next PC. jit_save_pc() saves that PC.
429+
let pc: *mut VALUE = unsafe {
430+
let cur_insn_len = insn_len(jit.get_opcode()) as isize;
431+
jit.get_pc().offset(cur_insn_len)
432+
};
433+
434+
let pc_to_cfunc = CodegenGlobals::get_pc_to_cfunc();
435+
match pc_to_cfunc.get(&pc) {
436+
Some(&(other_cme, _)) if other_cme != cme => {
437+
// Bail out if it's not the only cme on this callsite.
438+
incr_counter!(lazy_frame_failure);
439+
return false;
440+
}
441+
_ => {
442+
// Let rb_yjit_lazy_push_frame() lazily push a C frame on this PC.
443+
incr_counter!(lazy_frame_count);
444+
pc_to_cfunc.insert(pc, (cme, recv_idx));
445+
}
446+
}
447+
448+
// Save the PC to trigger a lazy frame push, and save the SP to get the receiver.
449+
// The C func may call a method that doesn't raise, so prepare for invalidation too.
450+
jit_prepare_non_leaf_call(jit, asm);
451+
452+
// Make sure we're ready for calling rb_vm_push_cfunc_frame().
453+
let cfunc_argc = unsafe { get_mct_argc(get_cme_def_body_cfunc(cme)) };
454+
if cfunc_argc != -1 {
455+
assert_eq!(recv_idx as i32, cfunc_argc); // verify the receiver index if possible
456+
}
457+
assert!(asm.get_leaf_ccall()); // It checks the stack canary we set for known_cfunc_codegen.
458+
459+
true
460+
}
461+
414462
/// jit_save_pc() + gen_save_sp(). Should be used before calling a routine that could:
415463
/// - Perform GC allocation
416464
/// - Take the VM lock through RB_VM_LOCK_ENTER()
@@ -5395,7 +5443,7 @@ fn jit_rb_str_byteslice(
53955443
asm: &mut Assembler,
53965444
_ocb: &mut OutlinedCb,
53975445
_ci: *const rb_callinfo,
5398-
_cme: *const rb_callable_method_entry_t,
5446+
cme: *const rb_callable_method_entry_t,
53995447
_block: Option<BlockHandler>,
54005448
argc: i32,
54015449
_known_recv_class: Option<VALUE>,
@@ -5409,7 +5457,9 @@ fn jit_rb_str_byteslice(
54095457
(Type::Fixnum, Type::Fixnum) => {},
54105458
// Raises when non-integers are passed in, which requires the method frame
54115459
// to be pushed for the backtrace
5412-
_ => return false,
5460+
_ => if !jit_prepare_lazy_frame_call(jit, asm, cme, StackOpnd(2)) {
5461+
return false;
5462+
}
54135463
}
54145464
asm_comment!(asm, "String#byteslice");
54155465

@@ -5431,11 +5481,11 @@ fn jit_rb_str_byteslice(
54315481
}
54325482

54335483
fn jit_rb_str_getbyte(
5434-
_jit: &mut JITState,
5484+
jit: &mut JITState,
54355485
asm: &mut Assembler,
54365486
_ocb: &mut OutlinedCb,
54375487
_ci: *const rb_callinfo,
5438-
_cme: *const rb_callable_method_entry_t,
5488+
cme: *const rb_callable_method_entry_t,
54395489
_block: Option<BlockHandler>,
54405490
_argc: i32,
54415491
_known_recv_class: Option<VALUE>,
@@ -5444,17 +5494,19 @@ fn jit_rb_str_getbyte(
54445494
fn rb_str_getbyte(str: VALUE, index: VALUE) -> VALUE;
54455495
}
54465496

5447-
let index = asm.stack_opnd(0);
5448-
let recv = asm.stack_opnd(1);
5449-
54505497
// rb_str_getbyte should be leaf if the index is a fixnum
5451-
if asm.ctx.get_opnd_type(index.into()) != Type::Fixnum {
5498+
if asm.ctx.get_opnd_type(StackOpnd(0)) != Type::Fixnum {
54525499
// Raises when non-integers are passed in, which requires the method frame
54535500
// to be pushed for the backtrace
5454-
return false;
5501+
if !jit_prepare_lazy_frame_call(jit, asm, cme, StackOpnd(1)) {
5502+
return false;
5503+
}
54555504
}
54565505
asm_comment!(asm, "String#getbyte");
54575506

5507+
let index = asm.stack_opnd(0);
5508+
let recv = asm.stack_opnd(1);
5509+
54585510
let ret_opnd = asm.ccall(rb_str_getbyte as *const u8, vec![recv, index]);
54595511
asm.stack_pop(2); // Keep them on stack during ccall for GC
54605512

@@ -5465,6 +5517,35 @@ fn jit_rb_str_getbyte(
54655517
true
54665518
}
54675519

5520+
fn jit_rb_str_setbyte(
5521+
jit: &mut JITState,
5522+
asm: &mut Assembler,
5523+
_ocb: &mut OutlinedCb,
5524+
_ci: *const rb_callinfo,
5525+
cme: *const rb_callable_method_entry_t,
5526+
_block: Option<BlockHandler>,
5527+
_argc: i32,
5528+
_known_recv_class: Option<VALUE>,
5529+
) -> bool {
5530+
// Raises when index is out of range. Lazily push a frame in that case.
5531+
if !jit_prepare_lazy_frame_call(jit, asm, cme, StackOpnd(2)) {
5532+
return false;
5533+
}
5534+
asm_comment!(asm, "String#setbyte");
5535+
5536+
let value = asm.stack_opnd(0);
5537+
let index = asm.stack_opnd(1);
5538+
let recv = asm.stack_opnd(2);
5539+
5540+
let ret_opnd = asm.ccall(rb_str_setbyte as *const u8, vec![recv, index, value]);
5541+
asm.stack_pop(3); // Keep them on stack during ccall for GC
5542+
5543+
let out_opnd = asm.stack_push(Type::UnknownImm);
5544+
asm.mov(out_opnd, ret_opnd);
5545+
5546+
true
5547+
}
5548+
54685549
// Codegen for rb_str_to_s()
54695550
// When String#to_s is called on a String instance, the method returns self and
54705551
// most of the overhead comes from setting up the method call. We observed that
@@ -9693,6 +9774,7 @@ pub fn yjit_reg_method_codegen_fns() {
96939774
yjit_reg_method(rb_cString, "size", jit_rb_str_length);
96949775
yjit_reg_method(rb_cString, "bytesize", jit_rb_str_bytesize);
96959776
yjit_reg_method(rb_cString, "getbyte", jit_rb_str_getbyte);
9777+
yjit_reg_method(rb_cString, "setbyte", jit_rb_str_setbyte);
96969778
yjit_reg_method(rb_cString, "byteslice", jit_rb_str_byteslice);
96979779
yjit_reg_method(rb_cString, "<<", jit_rb_str_concat);
96989780
yjit_reg_method(rb_cString, "+@", jit_rb_str_uplus);
@@ -9769,6 +9851,10 @@ pub struct CodegenGlobals {
97699851

97709852
/// Page indexes for outlined code that are not associated to any ISEQ.
97719853
ocb_pages: Vec<usize>,
9854+
9855+
/// Map of cfunc YARV PCs to CMEs and receiver indexes, used to lazily push
9856+
/// a frame when rb_yjit_lazy_push_frame() is called with a PC in this HashMap.
9857+
pc_to_cfunc: HashMap<*mut VALUE, (*const rb_callable_method_entry_t, u8)>,
97729858
}
97739859

97749860
/// For implementing global code invalidation. A position in the inline
@@ -9860,6 +9946,7 @@ impl CodegenGlobals {
98609946
entry_stub_hit_trampoline,
98619947
global_inval_patches: Vec::new(),
98629948
ocb_pages,
9949+
pc_to_cfunc: HashMap::new(),
98639950
};
98649951

98659952
// Initialize the codegen globals instance
@@ -9938,6 +10025,10 @@ impl CodegenGlobals {
993810025
pub fn get_ocb_pages() -> &'static Vec<usize> {
993910026
&CodegenGlobals::get_instance().ocb_pages
994010027
}
10028+
10029+
pub fn get_pc_to_cfunc() -> &'static mut HashMap<*mut VALUE, (*const rb_callable_method_entry_t, u8)> {
10030+
&mut CodegenGlobals::get_instance().pc_to_cfunc
10031+
}
994110032
}
994210033

994310034
#[cfg(test)]

yjit/src/cruby.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ extern "C" {
117117
ci: *const rb_callinfo,
118118
) -> *const rb_callable_method_entry_t;
119119
pub fn rb_hash_empty_p(hash: VALUE) -> VALUE;
120+
pub fn rb_str_setbyte(str: VALUE, index: VALUE, value: VALUE) -> VALUE;
120121
pub fn rb_vm_splat_array(flag: VALUE, ary: VALUE) -> VALUE;
121122
pub fn rb_vm_concat_array(ary1: VALUE, ary2st: VALUE) -> VALUE;
122123
pub fn rb_vm_concat_to_array(ary1: VALUE, ary2st: VALUE) -> VALUE;
@@ -142,6 +143,7 @@ extern "C" {
142143
) -> VALUE;
143144
pub fn rb_vm_ic_hit_p(ic: IC, reg_ep: *const VALUE) -> bool;
144145
pub fn rb_vm_stack_canary() -> VALUE;
146+
pub fn rb_vm_push_cfunc_frame(cme: *const rb_callable_method_entry_t, recv_idx: c_int);
145147
}
146148

147149
// Renames

0 commit comments

Comments
 (0)