ruby-changes:68847
From: Alan <ko1@a...>
Date: Thu, 21 Oct 2021 08:14:16 +0900 (JST)
Subject: [ruby-changes:68847] 0cd9120f17 (master): YJIT: hash specialization for opt_aref
https://git.ruby-lang.org/ruby.git/commit/?id=0cd9120f17 From 0cd9120f177b153126a093e4beabb5784cd0ab99 Mon Sep 17 00:00:00 2001 From: Alan Wu <XrXr@u...> Date: Thu, 11 Mar 2021 14:46:19 -0500 Subject: YJIT: hash specialization for opt_aref Make it lazy and add a hash specialization in addition to the array specialization. --- bootstraptest/test_yjit.rb | 87 +++++++++++++++++++ yjit.rb | 2 +- yjit_codegen.c | 203 +++++++++++++++++++++++++++++++++------------ yjit_iface.h | 2 - 4 files changed, 238 insertions(+), 56 deletions(-) diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb index 4750ac5fe8..44817313b4 100644 --- a/bootstraptest/test_yjit.rb +++ b/bootstraptest/test_yjit.rb @@ -361,3 +361,90 @@ assert_equal 'ok', %q{ https://github.com/ruby/ruby/blob/trunk/bootstraptest/test_yjit.rb#L361 subclasses.each { _1.new.get } parent.new.get } + +# Test polymorphic opt_aref. array -> hash +assert_equal '[42, :key]', %q{ + def index(obj, idx) + obj[idx] + end + + index([], 0) # get over compilation threshold + + [ + index([42], 0), + index({0=>:key}, 0), + ] +} + +# Test polymorphic opt_aref. hash -> array -> custom class +assert_equal '[nil, nil, :custom]', %q{ + def index(obj, idx) + obj[idx] + end + + custom = Object.new + def custom.[](_idx) + :custom + end + + index({}, 0) # get over compilation threshold + + [ + index({}, 0), + index([], 0), + index(custom, 0) + ] +} + +# Test polymorphic opt_aref. array -> custom class +assert_equal '[42, :custom]', %q{ + def index(obj, idx) + obj[idx] + end + + custom = Object.new + def custom.[](_idx) + :custom + end + + index([], 0) # get over compilation threshold + + [ + index([42], 0), + index(custom, 0) + ] +} + +# Test custom hash method with opt_aref +assert_equal '[nil, :ok]', %q{ + def index(obj, idx) + obj[idx] + end + + custom = Object.new + def custom.hash + 42 + end + + h = {custom => :ok} + + [ + index(h, 0), + index(h, custom) + ] +} + +# Test default value block for Hash with opt_aref +assert_equal '[42, :default]', %q{ + def index(obj, idx) + obj[idx] + end + + h = Hash.new { :default } + h[0] = 42 + + [ + index(h, 0), + index(h, 1) + ] +} diff --git a/yjit.rb b/yjit.rb index 5daee855e1..a05c2b2d24 100644 --- a/yjit.rb +++ b/yjit.rb @@ -14,7 +14,7 @@ module YJIT https://github.com/ruby/ruby/blob/trunk/yjit.rb#L14 # Sort the blocks by increasing addresses blocks.sort_by(&:address).each_with_index do |block, i| - str << "== BLOCK #{i+1}/#{blocks.length}: #{block.code.length} BYTES, ISEQ RANGE [#{block.iseq_start_index},#{block.iseq_end_index}] ".ljust(80, "=") + str << "== BLOCK #{i+1}/#{blocks.length}: #{block.code.length} BYTES, ISEQ RANGE [#{block.iseq_start_index},#{block.iseq_end_index}) ".ljust(80, "=") str << "\n" cs.disasm(block.code, block.address).each do |i| diff --git a/yjit_codegen.c b/yjit_codegen.c index 7d591a9147..e1d27c5b45 100644 --- a/yjit_codegen.c +++ b/yjit_codegen.c @@ -89,15 +89,16 @@ jit_at_current_insn(jitstate_t* jit) https://github.com/ruby/ruby/blob/trunk/yjit_codegen.c#L89 return (ec_pc == jit->pc); } -// Peek at the topmost value on the Ruby stack +// Peek at the nth topmost value on the Ruby stack. +// Returns the topmost value when n == 0. static VALUE -jit_peek_at_stack(jitstate_t* jit, ctx_t* ctx) +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; + VALUE *sp = jit->ec->cfp->sp + ctx->sp_offset; - return *(sp - 1); + return *(sp - 1 - n); } static VALUE @@ -621,7 +622,7 @@ enum jcc_kinds { https://github.com/ruby/ruby/blob/trunk/yjit_codegen.c#L622 // Generate a jump to a stub that recompiles the current YARV instruction on failure. // When depth_limitk is exceeded, generate a jump to a side exit. static void -jit_chain_guard(enum jcc_kinds jcc, jitstate_t *jit, ctx_t *ctx, uint8_t depth_limit, uint8_t *side_exit) +jit_chain_guard(enum jcc_kinds jcc, jitstate_t *jit, const ctx_t *ctx, uint8_t depth_limit, uint8_t *side_exit) { branchgen_fn target0_gen_fn; @@ -661,7 +662,8 @@ jit_chain_guard(enum jcc_kinds jcc, jitstate_t *jit, ctx_t *ctx, uint8_t depth_l https://github.com/ruby/ruby/blob/trunk/yjit_codegen.c#L662 bool rb_iv_index_tbl_lookup(struct st_table *iv_index_tbl, ID id, struct rb_iv_index_tbl_entry **ent); // vm_insnhelper.c enum { - GETIVAR_MAX_DEPTH = 10 // up to 5 different classes, and embedded or not for each + GETIVAR_MAX_DEPTH = 10, // up to 5 different classes, and embedded or not for each + OPT_AREF_MAX_CHAIN_DEPTH = 2, // hashes and arrays }; static codegen_status_t @@ -917,7 +919,7 @@ gen_opt_ge(jitstate_t* jit, ctx_t* ctx) https://github.com/ruby/ruby/blob/trunk/yjit_codegen.c#L919 } static codegen_status_t -gen_opt_aref(jitstate_t* jit, ctx_t* ctx) +gen_opt_aref(jitstate_t *jit, ctx_t *ctx) { struct rb_call_data * cd = (struct rb_call_data *)jit_get_arg(jit, 0); int32_t argc = (int32_t)vm_ci_argc(cd->ci); @@ -928,64 +930,159 @@ gen_opt_aref(jitstate_t* jit, ctx_t* ctx) https://github.com/ruby/ruby/blob/trunk/yjit_codegen.c#L930 return YJIT_CANT_COMPILE; } - const rb_callable_method_entry_t *cme = vm_cc_cme(cd->cc); - - // Bail if the inline cache has been filled. Currently, certain types - // (including arrays) don't use the inline cache, so if the inline cache - // has an entry, then this must be used by some other type. - if (cme) { - GEN_COUNTER_INC(cb, oaref_filled_cc); - return YJIT_CANT_COMPILE; + // Defer compilation so we can specialize base on a runtime receiver + if (!jit_at_current_insn(jit)) { + defer_compilation(jit->block, jit->insn_idx, ctx); + return YJIT_END_BLOCK; } + // Remember the context on entry for adding guard chains + const ctx_t starting_context = *ctx; + + // Specialize base on compile time values + VALUE comptime_idx = jit_peek_at_stack(jit, ctx, 0); + VALUE comptime_recv = jit_peek_at_stack(jit, ctx, 1); + // Create a size-exit to fall back to the interpreter - uint8_t* side_exit = yjit_side_exit(jit, ctx); + uint8_t *side_exit = yjit_side_exit(jit, ctx); - if (!assume_bop_not_redefined(jit->block, ARRAY_REDEFINED_OP_FLAG, BOP_AREF)) { - return YJIT_CANT_COMPILE; + if (CLASS_OF(comptime_recv) == rb_cArray && RB_FIXNUM_P(comptime_idx)) { + if (!assume_bop_not_redefined(jit->block, ARRAY_REDEFINED_OP_FLAG, BOP_AREF)) { + return YJIT_CANT_COMPILE; + } + + // Pop the stack operands + x86opnd_t idx_opnd = ctx_stack_pop(ctx, 1); + x86opnd_t recv_opnd = ctx_stack_pop(ctx, 1); + mov(cb, REG0, recv_opnd); + + // if (SPECIAL_CONST_P(recv)) { + // Bail if receiver is not a heap object + 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); + + // Bail if recv has a class other than ::Array. + // BOP_AREF check above is only good for ::Array. + mov(cb, REG1, mem_opnd(64, REG0, offsetof(struct RBasic, klass))); + mov(cb, REG0, const_ptr_opnd((void *)rb_cArray)); + cmp(cb, REG0, REG1); + jit_chain_guard(JCC_JNE, jit, &starting_context, OPT_AREF_MAX_CHAIN_DEPTH, side_exit); + + // Bail if idx is not a FIXNUM + mov(cb, REG1, idx_opnd); + test(cb, REG1, imm_opnd(RUBY_FIXNUM_FLAG)); + jz_ptr(cb, COUNTED_EXIT(side_exit, oaref_arg_not_fixnum)); + + // Call VALUE rb_ary_entry_internal(VALUE ary, long offset). + // It never raises or allocates, so we don't need to write to cfp->pc. + { + yjit_save_regs(cb); + + mov(cb, RDI, recv_opnd); + sar(cb, REG1, imm_opnd(1)); // Convert fixnum to int + mov(cb, RSI, REG1); + call_ptr(cb, REG0, (void *)rb_ary_entry_internal); + + yjit_load_regs(cb); + + // Push the return value onto the stack + x86opnd_t stack_ret = ctx_stack_push(ctx, T_NONE); + mov(cb, stack_ret, RAX); + } + + // Jump to next instruction. This allows guard chains to share the same successor. + { + ctx_t reset_depth = *ctx; + reset_depth.chain_depth = 0; + + blockid_t jump_block = { jit->iseq, jit_next_insn_idx(jit) }; + + // Generate the jump instruction + gen_direct_jump( + &reset_depth, + jump_block + ); + } + return YJIT_END_BLOCK; } + else if (CLASS_OF(comptime_recv) == rb_cHash) { + if (!assume_bop_not_redefined(jit->block, HASH_REDEFINED_OP_FLAG, BOP_AREF)) { + return YJIT_CANT_COMPILE; + } - // Pop the stack operands - x86opnd_t idx_opnd = ctx_stack_pop(ctx, 1); - x86opnd_t recv_opnd = ctx_stack_pop(ctx, 1); - mov(cb, REG0, recv_opnd); + // Pop the stack operands + x86opnd_t idx_opnd = ctx_stack_pop(ctx, 1); + x86opnd_t recv_opnd = ctx_stack_pop(ctx, 1); + mov(cb, REG0, recv_opnd); - // if (SPECIAL_CONST_P(recv)) { - // Bail if it's not a heap object - 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); - - // Bail if recv has a class other than ::Array. - // BOP_AREF check above is only good for ::Array. - mov(cb, REG1, mem_opnd(64, REG0, offsetof(struct RBasic, klass))); - mov(cb, REG0, const_ptr_opnd((void *)rb_cArray)); - cmp(cb, REG0, REG1); - jne_ptr(cb, COUNTED_EXIT(side_exit, oaref_not_array)); - - // Bai (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/