1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00

Polymorphic opt_send_without_block

This commit is contained in:
Alan Wu 2021-03-22 20:12:34 -04:00
parent ec1cbbb07d
commit a8f7eb2f35
4 changed files with 151 additions and 79 deletions

View file

@ -448,3 +448,60 @@ assert_equal '[42, :default]', %q{
index(h, 1)
]
}
# A regression test for making sure cfp->sp is proper when
# hitting stubs. See :stub-sp-flush:
assert_equal 'ok', %q{
class D
def foo
Object.new
end
end
GC.stress = true
10.times do
D.new.foo
# ^
# This hits a stub with sp_offset > 0
end
:ok
}
# Test polymorphic callsite, cfunc -> iseq
assert_equal '[Cfunc, Iseq]', %q{
public def call_itself
itself # the polymorphic callsite
end
class Cfunc; end
class Iseq
def itself
self
end
end
call_itself # cross threshold
[Cfunc.call_itself, Iseq.call_itself]
}
# Test polymorphic callsite, iseq -> cfunc
assert_equal '[Iseq, Cfunc]', %q{
public def call_itself
itself # the polymorphic callsite
end
class Cfunc; end
class Iseq
def itself
self
end
end
call_itself # cross threshold
[Iseq.call_itself, Cfunc.call_itself]
}

View file

@ -96,7 +96,11 @@ jit_peek_at_stack(jitstate_t* jit, ctx_t* ctx, int n)
{
RUBY_ASSERT(jit_at_current_insn(jit));
VALUE *sp = jit->ec->cfp->sp + ctx->sp_offset;
// Note: this does not account for ctx->sp_offset because
// this is only available when hitting a stub, and while
// hitting a stub, cfp->sp needs to be up to date in case
// codegen functions trigger GC. See :stub-sp-flush:.
VALUE *sp = jit->ec->cfp->sp;
return *(sp - 1 - n);
}
@ -664,6 +668,7 @@ bool rb_iv_index_tbl_lookup(struct st_table *iv_index_tbl, ID id, struct rb_iv_i
enum {
GETIVAR_MAX_DEPTH = 10, // up to 5 different classes, and embedded or not for each
OPT_AREF_MAX_CHAIN_DEPTH = 2, // hashes and arrays
OSWB_MAX_DEPTH = 5, // up to 5 different classes
};
static codegen_status_t
@ -1346,15 +1351,13 @@ gen_oswb_cfunc(jitstate_t* jit, ctx_t* ctx, struct rb_call_data * cd, const rb_c
const rb_method_cfunc_t *cfunc = UNALIGNED_MEMBER_PTR(cme->def, body.cfunc);
// If the function expects a Ruby array of arguments
if (cfunc->argc < 0 && cfunc->argc != -1)
{
if (cfunc->argc < 0 && cfunc->argc != -1) {
GEN_COUNTER_INC(cb, oswb_cfunc_ruby_array_varg);
return YJIT_CANT_COMPILE;
}
// If the argument count doesn't match
if (cfunc->argc >= 0 && cfunc->argc != argc)
{
if (cfunc->argc >= 0 && cfunc->argc != argc) {
GEN_COUNTER_INC(cb, oswb_cfunc_argc_mismatch);
return YJIT_CANT_COMPILE;
}
@ -1373,38 +1376,6 @@ gen_oswb_cfunc(jitstate_t* jit, ctx_t* ctx, struct rb_call_data * cd, const rb_c
// Points to the receiver operand on the stack
x86opnd_t recv = ctx_stack_opnd(ctx, argc);
mov(cb, REG0, recv);
// Callee method ID
//ID mid = vm_ci_mid(cd->ci);
//printf("JITting call to C function \"%s\", argc: %lu\n", rb_id2name(mid), argc);
//print_str(cb, "");
//print_str(cb, "calling CFUNC:");
//print_str(cb, rb_id2name(mid));
//print_str(cb, "recv");
//print_ptr(cb, recv);
// Check that the receiver is a heap object
{
uint8_t *receiver_not_heap = COUNTED_EXIT(side_exit, oswb_se_receiver_not_heap);
test(cb, REG0, imm_opnd(RUBY_IMMEDIATE_MASK));
jnz_ptr(cb, receiver_not_heap);
cmp(cb, REG0, imm_opnd(Qfalse));
je_ptr(cb, receiver_not_heap);
cmp(cb, REG0, imm_opnd(Qnil));
je_ptr(cb, receiver_not_heap);
}
// Pointer to the klass field of the receiver &(recv->klass)
x86opnd_t klass_opnd = mem_opnd(64, REG0, offsetof(struct RBasic, klass));
// FIXME: This leaks when st_insert raises NoMemoryError
assume_method_lookup_stable(cd->cc->klass, cme, jit->block);
// Bail if receiver class is different from compile-time call cache class
jit_mov_gc_ptr(jit, cb, REG1, (VALUE)cd->cc->klass);
cmp(cb, klass_opnd, REG1);
jne_ptr(cb, COUNTED_EXIT(side_exit, oswb_se_cc_klass_differ));
// Store incremented PC into current control frame in case callee raises.
mov(cb, REG0, const_ptr_opnd(jit->pc + insn_len(BIN(opt_send_without_block))));
@ -1545,9 +1516,14 @@ gen_oswb_cfunc(jitstate_t* jit, ctx_t* ctx, struct rb_call_data * cd, const rb_c
);
}
// TODO: gen_oswb_iseq() jumps to the next instruction with ctx->sp_offset == 0
// after the call, while this does not. This difference prevents
// the two call types from sharing the same successor.
// Jump (fall through) to the call continuation block
// We do this to end the current block after the call
blockid_t cont_block = { jit->iseq, jit_next_idx(jit) };
blockid_t cont_block = { jit->iseq, jit_next_insn_idx(jit) };
ctx->chain_depth = 0;
gen_direct_jump(
ctx,
cont_block
@ -1609,35 +1585,6 @@ gen_oswb_iseq(jitstate_t* jit, ctx_t* ctx, struct rb_call_data * cd, const rb_ca
// Points to the receiver operand on the stack
x86opnd_t recv = ctx_stack_opnd(ctx, argc);
mov(cb, REG0, recv);
// Callee method ID
//ID mid = vm_ci_mid(cd->ci);
//printf("JITting call to Ruby function \"%s\", argc: %d\n", rb_id2name(mid), argc);
//print_str(cb, "");
//print_str(cb, "recv");
//print_ptr(cb, recv);
// Check that the receiver is a heap object
{
uint8_t *receiver_not_heap = COUNTED_EXIT(side_exit, oswb_se_receiver_not_heap);
test(cb, REG0, imm_opnd(RUBY_IMMEDIATE_MASK));
jnz_ptr(cb, receiver_not_heap);
cmp(cb, REG0, imm_opnd(Qfalse));
je_ptr(cb, receiver_not_heap);
cmp(cb, REG0, imm_opnd(Qnil));
je_ptr(cb, receiver_not_heap);
}
// Pointer to the klass field of the receiver &(recv->klass)
x86opnd_t klass_opnd = mem_opnd(64, REG0, offsetof(struct RBasic, klass));
assume_method_lookup_stable(cd->cc->klass, cme, jit->block);
// Bail if receiver class is different from compile-time call cache class
jit_mov_gc_ptr(jit, cb, REG1, (VALUE)cd->cc->klass);
cmp(cb, klass_opnd, REG1);
jne_ptr(cb, COUNTED_EXIT(side_exit, oswb_se_cc_klass_differ));
if (METHOD_ENTRY_VISI(cme) == METHOD_VISI_PROTECTED) {
// Generate ancestry guard for protected callee.
@ -1717,6 +1664,7 @@ gen_oswb_iseq(jitstate_t* jit, ctx_t* ctx, struct rb_call_data * cd, const rb_ca
ctx_stack_pop(&return_ctx, argc + 1);
ctx_stack_push(&return_ctx, T_NONE);
return_ctx.sp_offset = 0;
return_ctx.chain_depth = 0;
// Write the JIT return address on the callee frame
gen_branch(
@ -1749,39 +1697,92 @@ gen_opt_send_without_block(jitstate_t* jit, ctx_t* ctx)
// Relevant definitions:
// rb_execution_context_t : vm_core.h
// invoker, cfunc logic : method.h, vm_method.c
// rb_callinfo : vm_callinfo.h
// rb_callable_method_entry_t : method.h
// vm_call_cfunc_with_frame : vm_insnhelper.c
// rb_callcache : vm_callinfo.h
struct rb_call_data * cd = (struct rb_call_data *)jit_get_arg(jit, 0);
struct rb_call_data *cd = (struct rb_call_data *)jit_get_arg(jit, 0);
const struct rb_callinfo *ci = cd->ci; // info about the call site
int32_t argc = (int32_t)vm_ci_argc(cd->ci);
ID mid = vm_ci_mid(cd->ci);
// Don't JIT calls with keyword splat
if (vm_ci_flag(cd->ci) & VM_CALL_KW_SPLAT) {
if (vm_ci_flag(ci) & VM_CALL_KW_SPLAT) {
GEN_COUNTER_INC(cb, oswb_kw_splat);
return YJIT_CANT_COMPILE;
}
// Don't JIT calls that aren't simple
if (!(vm_ci_flag(cd->ci) & VM_CALL_ARGS_SIMPLE)) {
if (!(vm_ci_flag(ci) & VM_CALL_ARGS_SIMPLE)) {
GEN_COUNTER_INC(cb, oswb_callsite_not_simple);
return YJIT_CANT_COMPILE;
}
// Don't JIT if the inline cache is not set
if (!cd->cc || !cd->cc->klass) {
GEN_COUNTER_INC(cb, oswb_ic_empty);
// Defer compilation so we can specialize on class of receiver
if (!jit_at_current_insn(jit)) {
defer_compilation(jit->block, jit->insn_idx, ctx);
return YJIT_END_BLOCK;
}
VALUE comptime_recv = jit_peek_at_stack(jit, ctx, argc);
//rp(comptime_recv);
//fprintf(stderr, "offset=%d\n", (int)ctx->sp_offset);
VALUE comptime_recv_klass = CLASS_OF(comptime_recv);
// Can't guard for for these classes because some of they are sometimes
// immediate (special const). Can remove this once jit_guard_known_class is able to hanlde them.
if (comptime_recv_klass == rb_cInteger || comptime_recv_klass == rb_cSymbol || comptime_recv_klass == rb_cFloat) {
return YJIT_CANT_COMPILE;
}
const rb_callable_method_entry_t *cme = vm_cc_cme(cd->cc);
// Don't JIT if the method entry is out of date
if (METHOD_ENTRY_INVALIDATED(cme)) {
GEN_COUNTER_INC(cb, oswb_invalid_cme);
// Do method lookup
const rb_callable_method_entry_t *cme = rb_callable_method_entry(comptime_recv_klass, mid);
if (!cme) {
// TODO: counter
return YJIT_CANT_COMPILE;
}
RUBY_ASSERT(cme->called_id == mid);
assume_method_lookup_stable(comptime_recv_klass, cme, jit->block);
// Known class guard. jit_konwn_klass
{
uint8_t *side_exit = yjit_side_exit(jit, ctx);
// Points to the receiver operand on the stack
x86opnd_t recv = ctx_stack_opnd(ctx, argc);
mov(cb, REG0, recv);
// Callee method ID
//ID mid = vm_ci_mid(cd->ci);
//printf("JITting call to C function \"%s\", argc: %lu\n", rb_id2name(mid), argc);
//print_str(cb, "");
//print_str(cb, "calling CFUNC:");
//print_str(cb, rb_id2name(mid));
//print_str(cb, "recv");
//print_ptr(cb, recv);
// Check that the receiver is a heap object
{
// uint8_t *receiver_not_heap = COUNTED_EXIT(side_exit, oswb_se_receiver_not_heap);
test(cb, REG0, imm_opnd(RUBY_IMMEDIATE_MASK));
jnz_ptr(cb, side_exit);
cmp(cb, REG0, imm_opnd(Qfalse));
je_ptr(cb, side_exit);
cmp(cb, REG0, imm_opnd(Qnil));
je_ptr(cb, side_exit);
}
// Pointer to the klass field of the receiver &(recv->klass)
x86opnd_t klass_opnd = mem_opnd(64, REG0, offsetof(struct RBasic, klass));
// Bail if receiver class is different from compile-time call cache class
jit_mov_gc_ptr(jit, cb, REG1, comptime_recv_klass);
cmp(cb, klass_opnd, REG1);
jit_chain_guard(JCC_JNE, jit, ctx, OSWB_MAX_DEPTH, COUNTED_EXIT(side_exit, oswb_se_cc_klass_differ));
}
switch (cme->def->type) {
case VM_METHOD_TYPE_ISEQ:
return gen_oswb_iseq(jit, ctx, cd, cme, argc);

View file

@ -354,12 +354,22 @@ branch_stub_hit(uint32_t branch_idx, uint32_t target_idx, rb_execution_context_t
RB_VM_LOCK_ENTER();
RUBY_ASSERT(branch_idx < num_branches);
RUBY_ASSERT(target_idx < 2);
branch_t *branch = &branch_entries[branch_idx];
blockid_t target = branch->targets[target_idx];
const ctx_t* target_ctx = &branch->target_ctxs[target_idx];
// :stub-sp-flush:
// Generated code do stack operations without modifying cfp->sp, while the
// cfp->sp tells the GC what values on the stack to root. Generated code
// generally takes care of updating cfp->sp when it calls runtime routines that
// could trigger GC, but for the case of branch stubs, it's inconvenient. So
// we do it here.
VALUE *const original_interp_sp = ec->cfp->sp;
ec->cfp->sp += target_ctx->sp_offset;
//fprintf(stderr, "\nstub hit, branch idx: %d, target idx: %d\n", branch_idx, target_idx);
//fprintf(stderr, "blockid.iseq=%p, blockid.idx=%d\n", target.iseq, target.idx);
//fprintf(stderr, "chain_depth=%d\n", target_ctx->chain_depth);
@ -415,6 +425,8 @@ branch_stub_hit(uint32_t branch_idx, uint32_t target_idx, rb_execution_context_t
branch->end_pos = cb->write_pos;
cb_set_pos(cb, cur_pos);
// Restore interpreter sp, since the code hitting the stub expects the original.
ec->cfp->sp = original_interp_sp;
RB_VM_LOCK_LEAVE();
// Return a pointer to the compiled block version

View file

@ -233,6 +233,8 @@ add_lookup_dependency_i(st_data_t *key, st_data_t *value, st_data_t data, int ex
// cme is vald.
// When either of these assumptions becomes invalid, rb_yjit_method_lookup_change() or
// rb_yjit_cme_invalidate() invalidates the block.
//
// @raise NoMemoryError
void
assume_method_lookup_stable(VALUE receiver_klass, const rb_callable_method_entry_t *cme, block_t *block)
{