mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 16:36:20 +08:00
[PATCH] ZJIT: Drop has_blockiseq guard on local variable access (#16113) So `has_blockiseq` was masking two other bugs. First, that we had only a static EP escape check for an iseq. We need to check both that an iseq is not known to statically escape them *and that it has not escaped them before*. Only then can we use SSA locals. This popped up in a weird PP unit test that was reducible to the test cases added in the PR. So I fixed that. Second, that we didn't VM lock when code patching for EP escape. That showed up in the Ractor btests in my previous PRs #14600 and #15254 #15316, and I had no idea what was going on at the time, so I dropped it. But if we add this lock, like all the other invariant patching cases have, the problem goes away. Seems reasonable. Finally, we can use normal full SSA locals even in the presence of a blockiseq. We still reload all of them after each send-with-block, but we can in a later PR reduce that to just the locals that are (recursively) visibly written to. [PATCH] [Bug #22224] Ractors, YJIT: Skip re-reporting recorded EP escapes to the JITs (#18176) * [22224] Ractors, YJIT: Skip re-reporting recorded EP escapes to the JITs vm_make_env_each reports every EP escape, and both rb_yjit_invalidate_ep_is_bp and rb_zjit_invalidate_no_ep_escape take the VM lock with a stop-all-Ractors barrier on every call. For a given iseq only the first report does anything: afterwards YJIT's no_ep_escape_iseqs entry is permanently an empty set and ZJIT's ep_escape_iseqs insert is a no-op, so the barrier is paid for nothing. Under multiple Ractors this serializes the whole process on every Proc created from an already-escaped iseq: a workload creating one lambda per iteration runs 150x slower on 8 Ractors than on 1 with YJIT enabled. Record the report in a jit_ep_escape_recorded flag on the iseq body, set by the active JIT while it holds the VM lock, and skip the calls in vm_make_env_each once it is set. The flag sits in existing padding and the body is ZALLOC'd, so it starts false and dies with the iseq. Setting it in the JIT rather than in the caller keeps mid-process RubyVM::YJIT.enable correct: escapes reported while no JIT is initialized are recorded by nobody and must not set the flag. The unlocked read is benign: a stale false causes one redundant, idempotent report. On the micro-benchmark from the bug, --yjit at 8 Ractors goes from 0.04M to 11.07M iterations/s (277x). * [22224] Deduplicate the EP escape record onto the iseq body The jit_ep_escape_recorded flag introduced in the previous commit is now the JITs' only record of an EP escape: YJIT's no_ep_escape_iseqs keeps only iseqs with live assumptions (the empty-set escape markers are gone), and ZJIT's ep_escape_iseqs set is removed together with its update_ep_escape_iseqs compaction pass, since the flag lives in the iseq body, which the GC does not move. Both JITs read the flag back at compile time through a new rb_jit_iseq_ep_escape_recorded_p helper. Co-authored-by: Alan Wu <XrXr@users.noreply.github.com> Only the two correctness changes of9efe557da5are backported: the runtime EP-escape check in iseq_to_hir() and the VM lock in rb_zjit_invalidate_no_ep_escape(). Without the former, suppressing repeat escape reports would leave NoEPEscape patch points registered after an escape never invalidated, because ZJIT here registers them unconditionally. Its has_blockiseq removal is an optimization and is left out; keeping has_blockiseq is strictly more conservative. ZJIT here keeps the name invariants::iseq_escapes_ep() rather than renaming it to iseq_seen_ep_escape(), sincee0184fff1ais not backported.
806 lines
19 KiB
C
806 lines
19 KiB
C
// Glue code shared between YJIT and ZJIT for use from Rust.
|
|
// For FFI safety and bindgen compatibility reasons, certain types of C
|
|
// functions require wrapping before they can be called from Rust. Those show
|
|
// up here.
|
|
//
|
|
// Code specific to YJIT and ZJIT should go to yjit.c and zjit.c respectively.
|
|
|
|
#include "internal.h"
|
|
#include "vm_core.h"
|
|
#include "vm_callinfo.h"
|
|
#include "builtin.h"
|
|
#include "insns.inc"
|
|
#include "insns_info.inc"
|
|
#include "iseq.h"
|
|
#include "internal/gc.h"
|
|
#include "vm_sync.h"
|
|
#include "internal/fixnum.h"
|
|
#include "internal/string.h"
|
|
|
|
enum jit_bindgen_constants {
|
|
// Field offsets for the RObject struct
|
|
ROBJECT_OFFSET_AS_HEAP_FIELDS = offsetof(struct RObject, as.heap.fields),
|
|
ROBJECT_OFFSET_AS_ARY = offsetof(struct RObject, as.ary),
|
|
|
|
// Field offsets for the RString struct
|
|
RUBY_OFFSET_RSTRING_LEN = offsetof(struct RString, len),
|
|
|
|
// Field offsets for rb_execution_context_t
|
|
RUBY_OFFSET_EC_CFP = offsetof(rb_execution_context_t, cfp),
|
|
RUBY_OFFSET_EC_INTERRUPT_FLAG = offsetof(rb_execution_context_t, interrupt_flag),
|
|
RUBY_OFFSET_EC_INTERRUPT_MASK = offsetof(rb_execution_context_t, interrupt_mask),
|
|
RUBY_OFFSET_EC_THREAD_PTR = offsetof(rb_execution_context_t, thread_ptr),
|
|
RUBY_OFFSET_EC_RACTOR_ID = offsetof(rb_execution_context_t, ractor_id),
|
|
};
|
|
|
|
// Manually bound in rust since this is out-of-range of `int`,
|
|
// so this can't be in a `enum`, and we avoid `static const`
|
|
// to avoid allocating storage for the constant.
|
|
const shape_id_t rb_invalid_shape_id = INVALID_SHAPE_ID;
|
|
|
|
unsigned int
|
|
rb_iseq_encoded_size(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->iseq_size;
|
|
}
|
|
|
|
// Get the PC for a given index in an iseq
|
|
VALUE *
|
|
rb_iseq_pc_at_idx(const rb_iseq_t *iseq, uint32_t insn_idx)
|
|
{
|
|
RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq));
|
|
RUBY_ASSERT_ALWAYS(insn_idx < iseq->body->iseq_size);
|
|
VALUE *encoded = iseq->body->iseq_encoded;
|
|
VALUE *pc = &encoded[insn_idx];
|
|
return pc;
|
|
}
|
|
|
|
// Get the opcode given a program counter. Can return trace opcode variants.
|
|
int
|
|
rb_iseq_opcode_at_pc(const rb_iseq_t *iseq, const VALUE *pc)
|
|
{
|
|
// YJIT should only use iseqs after AST to bytecode compilation.
|
|
// (Certain non-default interpreter configurations never set ISEQ_TRANSLATED)
|
|
if (OPT_DIRECT_THREADED_CODE || OPT_CALL_THREADED_CODE) {
|
|
RUBY_ASSERT_ALWAYS(FL_TEST_RAW((VALUE)iseq, ISEQ_TRANSLATED));
|
|
}
|
|
|
|
const VALUE at_pc = *pc;
|
|
return rb_vm_insn_addr2opcode((const void *)at_pc);
|
|
}
|
|
|
|
unsigned long
|
|
rb_RSTRING_LEN(VALUE str)
|
|
{
|
|
return RSTRING_LEN(str);
|
|
}
|
|
|
|
char *
|
|
rb_RSTRING_PTR(VALUE str)
|
|
{
|
|
return RSTRING_PTR(str);
|
|
}
|
|
|
|
const char *
|
|
rb_insn_name(VALUE insn)
|
|
{
|
|
return insn_name(insn);
|
|
}
|
|
|
|
unsigned int
|
|
rb_vm_ci_argc(const struct rb_callinfo *ci)
|
|
{
|
|
return vm_ci_argc(ci);
|
|
}
|
|
|
|
ID
|
|
rb_vm_ci_mid(const struct rb_callinfo *ci)
|
|
{
|
|
return vm_ci_mid(ci);
|
|
}
|
|
|
|
unsigned int
|
|
rb_vm_ci_flag(const struct rb_callinfo *ci)
|
|
{
|
|
return vm_ci_flag(ci);
|
|
}
|
|
|
|
const struct rb_callinfo_kwarg *
|
|
rb_vm_ci_kwarg(const struct rb_callinfo *ci)
|
|
{
|
|
return vm_ci_kwarg(ci);
|
|
}
|
|
|
|
int
|
|
rb_get_cikw_keyword_len(const struct rb_callinfo_kwarg *cikw)
|
|
{
|
|
return cikw->keyword_len;
|
|
}
|
|
|
|
VALUE
|
|
rb_get_cikw_keywords_idx(const struct rb_callinfo_kwarg *cikw, int idx)
|
|
{
|
|
return cikw->keywords[idx];
|
|
}
|
|
|
|
rb_method_visibility_t
|
|
rb_METHOD_ENTRY_VISI(const rb_callable_method_entry_t *me)
|
|
{
|
|
return METHOD_ENTRY_VISI(me);
|
|
}
|
|
|
|
rb_method_type_t
|
|
rb_get_cme_def_type(const rb_callable_method_entry_t *cme)
|
|
{
|
|
if (UNDEFINED_METHOD_ENTRY_P(cme)) {
|
|
return VM_METHOD_TYPE_UNDEF;
|
|
}
|
|
else {
|
|
return cme->def->type;
|
|
}
|
|
}
|
|
|
|
ID
|
|
rb_get_cme_def_body_attr_id(const rb_callable_method_entry_t *cme)
|
|
{
|
|
return cme->def->body.attr.id;
|
|
}
|
|
|
|
enum method_optimized_type
|
|
rb_get_cme_def_body_optimized_type(const rb_callable_method_entry_t *cme)
|
|
{
|
|
return cme->def->body.optimized.type;
|
|
}
|
|
|
|
unsigned int
|
|
rb_get_cme_def_body_optimized_index(const rb_callable_method_entry_t *cme)
|
|
{
|
|
return cme->def->body.optimized.index;
|
|
}
|
|
|
|
rb_method_cfunc_t *
|
|
rb_get_cme_def_body_cfunc(const rb_callable_method_entry_t *cme)
|
|
{
|
|
return UNALIGNED_MEMBER_PTR(cme->def, body.cfunc);
|
|
}
|
|
|
|
uintptr_t
|
|
rb_get_def_method_serial(const rb_method_definition_t *def)
|
|
{
|
|
return def->method_serial;
|
|
}
|
|
|
|
ID
|
|
rb_get_def_original_id(const rb_method_definition_t *def)
|
|
{
|
|
return def->original_id;
|
|
}
|
|
|
|
VALUE
|
|
rb_get_def_bmethod_proc(rb_method_definition_t *def)
|
|
{
|
|
RUBY_ASSERT(def->type == VM_METHOD_TYPE_BMETHOD);
|
|
return def->body.bmethod.proc;
|
|
}
|
|
|
|
rb_proc_t *
|
|
rb_jit_get_proc_ptr(VALUE procv)
|
|
{
|
|
rb_proc_t *proc;
|
|
GetProcPtr(procv, proc);
|
|
return proc;
|
|
}
|
|
|
|
unsigned int
|
|
rb_jit_iseq_builtin_attrs(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->builtin_attrs;
|
|
}
|
|
|
|
// Relaxed memory ordering, but called by the JIT with VM lock and barrier.
|
|
void
|
|
rb_jit_iseq_mark_ep_escape_recorded(const rb_iseq_t *iseq)
|
|
{
|
|
rbimpl_atomic_store(&iseq->body->jit_ep_escape_recorded, 1, RBIMPL_ATOMIC_RELAXED);
|
|
}
|
|
|
|
// Whether an EP escape of this iseq has been reported to the enabled JIT.
|
|
bool
|
|
rb_jit_iseq_ep_escape_recorded_p(const rb_iseq_t *iseq)
|
|
{
|
|
return rbimpl_atomic_load(&iseq->body->jit_ep_escape_recorded, RBIMPL_ATOMIC_RELAXED) != 0;
|
|
}
|
|
|
|
int
|
|
rb_get_mct_argc(const rb_method_cfunc_t *mct)
|
|
{
|
|
return mct->argc;
|
|
}
|
|
|
|
void *
|
|
rb_get_mct_func(const rb_method_cfunc_t *mct)
|
|
{
|
|
return (void*)(uintptr_t)mct->func; // this field is defined as type VALUE (*func)(ANYARGS)
|
|
}
|
|
|
|
const rb_iseq_t *
|
|
rb_get_def_iseq_ptr(rb_method_definition_t *def)
|
|
{
|
|
return def_iseq_ptr(def);
|
|
}
|
|
|
|
const rb_iseq_t *
|
|
rb_get_iseq_body_local_iseq(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->local_iseq;
|
|
}
|
|
|
|
const rb_iseq_t *
|
|
rb_get_iseq_body_parent_iseq(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->parent_iseq;
|
|
}
|
|
|
|
unsigned int
|
|
rb_get_iseq_body_local_table_size(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->local_table_size;
|
|
}
|
|
|
|
VALUE *
|
|
rb_get_iseq_body_iseq_encoded(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->iseq_encoded;
|
|
}
|
|
|
|
unsigned
|
|
rb_get_iseq_body_stack_max(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->stack_max;
|
|
}
|
|
|
|
enum rb_iseq_type
|
|
rb_get_iseq_body_type(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->type;
|
|
}
|
|
|
|
bool
|
|
rb_get_iseq_flags_has_lead(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.flags.has_lead;
|
|
}
|
|
|
|
bool
|
|
rb_get_iseq_flags_has_opt(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.flags.has_opt;
|
|
}
|
|
|
|
bool
|
|
rb_get_iseq_flags_has_kw(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.flags.has_kw;
|
|
}
|
|
|
|
bool
|
|
rb_get_iseq_flags_has_post(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.flags.has_post;
|
|
}
|
|
|
|
bool
|
|
rb_get_iseq_flags_has_kwrest(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.flags.has_kwrest;
|
|
}
|
|
|
|
bool
|
|
rb_get_iseq_flags_anon_kwrest(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.flags.anon_kwrest;
|
|
}
|
|
|
|
bool
|
|
rb_get_iseq_flags_has_rest(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.flags.has_rest;
|
|
}
|
|
|
|
bool
|
|
rb_get_iseq_flags_ruby2_keywords(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.flags.ruby2_keywords;
|
|
}
|
|
|
|
bool
|
|
rb_get_iseq_flags_has_block(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.flags.has_block;
|
|
}
|
|
|
|
bool
|
|
rb_get_iseq_flags_ambiguous_param0(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.flags.ambiguous_param0;
|
|
}
|
|
|
|
bool
|
|
rb_get_iseq_flags_accepts_no_kwarg(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.flags.accepts_no_kwarg;
|
|
}
|
|
|
|
bool
|
|
rb_get_iseq_flags_forwardable(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.flags.forwardable;
|
|
}
|
|
|
|
// This is defined only as a named struct inside rb_iseq_constant_body.
|
|
// By giving it a separate typedef, we make it nameable by rust-bindgen.
|
|
// Bindgen's temp/anon name isn't guaranteed stable.
|
|
typedef struct rb_iseq_param_keyword rb_iseq_param_keyword_struct;
|
|
|
|
const rb_iseq_param_keyword_struct *
|
|
rb_get_iseq_body_param_keyword(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.keyword;
|
|
}
|
|
|
|
unsigned
|
|
rb_get_iseq_body_param_size(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.size;
|
|
}
|
|
|
|
int
|
|
rb_get_iseq_body_param_lead_num(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.lead_num;
|
|
}
|
|
|
|
int
|
|
rb_get_iseq_body_param_opt_num(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.opt_num;
|
|
}
|
|
|
|
const VALUE *
|
|
rb_get_iseq_body_param_opt_table(const rb_iseq_t *iseq)
|
|
{
|
|
return iseq->body->param.opt_table;
|
|
}
|
|
|
|
struct rb_control_frame_struct *
|
|
rb_get_ec_cfp(const rb_execution_context_t *ec)
|
|
{
|
|
return ec->cfp;
|
|
}
|
|
|
|
const rb_iseq_t *
|
|
rb_get_cfp_iseq(struct rb_control_frame_struct *cfp)
|
|
{
|
|
return cfp->iseq;
|
|
}
|
|
|
|
VALUE *
|
|
rb_get_cfp_pc(struct rb_control_frame_struct *cfp)
|
|
{
|
|
return (VALUE*)cfp->pc;
|
|
}
|
|
|
|
VALUE *
|
|
rb_get_cfp_sp(struct rb_control_frame_struct *cfp)
|
|
{
|
|
return cfp->sp;
|
|
}
|
|
|
|
VALUE
|
|
rb_get_cfp_self(struct rb_control_frame_struct *cfp)
|
|
{
|
|
return cfp->self;
|
|
}
|
|
|
|
VALUE *
|
|
rb_get_cfp_ep(struct rb_control_frame_struct *cfp)
|
|
{
|
|
return (VALUE*)cfp->ep;
|
|
}
|
|
|
|
const VALUE *
|
|
rb_get_cfp_ep_level(struct rb_control_frame_struct *cfp, uint32_t lv)
|
|
{
|
|
uint32_t i;
|
|
const VALUE *ep = (VALUE*)cfp->ep;
|
|
for (i = 0; i < lv; i++) {
|
|
ep = VM_ENV_PREV_EP(ep);
|
|
}
|
|
return ep;
|
|
}
|
|
|
|
VALUE
|
|
rb_yarv_class_of(VALUE obj)
|
|
{
|
|
return rb_class_of(obj);
|
|
}
|
|
|
|
// The FL_TEST() macro
|
|
VALUE
|
|
rb_FL_TEST(VALUE obj, VALUE flags)
|
|
{
|
|
return RB_FL_TEST(obj, flags);
|
|
}
|
|
|
|
// The FL_TEST_RAW() macro, normally an internal implementation detail
|
|
VALUE
|
|
rb_FL_TEST_RAW(VALUE obj, VALUE flags)
|
|
{
|
|
return FL_TEST_RAW(obj, flags);
|
|
}
|
|
|
|
// The RB_TYPE_P macro
|
|
bool
|
|
rb_RB_TYPE_P(VALUE obj, enum ruby_value_type t)
|
|
{
|
|
return RB_TYPE_P(obj, t);
|
|
}
|
|
|
|
long
|
|
rb_RSTRUCT_LEN(VALUE st)
|
|
{
|
|
return RSTRUCT_LEN(st);
|
|
}
|
|
|
|
const struct rb_callinfo *
|
|
rb_get_call_data_ci(const struct rb_call_data *cd)
|
|
{
|
|
return cd->ci;
|
|
}
|
|
|
|
bool
|
|
rb_BASIC_OP_UNREDEFINED_P(enum ruby_basic_operators bop, uint32_t klass)
|
|
{
|
|
return BASIC_OP_UNREDEFINED_P(bop, klass);
|
|
}
|
|
|
|
VALUE
|
|
rb_RCLASS_ORIGIN(VALUE c)
|
|
{
|
|
return RCLASS_ORIGIN(c);
|
|
}
|
|
|
|
// For debug builds
|
|
void
|
|
rb_assert_iseq_handle(VALUE handle)
|
|
{
|
|
RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(handle, imemo_iseq));
|
|
}
|
|
|
|
// Assert that we have the VM lock. Relevant mostly for multi ractor situations.
|
|
// The GC takes the lock before calling us, and this asserts that it indeed happens.
|
|
void
|
|
rb_assert_holding_vm_lock(void)
|
|
{
|
|
ASSERT_vm_locking();
|
|
}
|
|
|
|
int
|
|
rb_IMEMO_TYPE_P(VALUE imemo, enum imemo_type imemo_type)
|
|
{
|
|
return IMEMO_TYPE_P(imemo, imemo_type);
|
|
}
|
|
|
|
void
|
|
rb_assert_cme_handle(VALUE handle)
|
|
{
|
|
RUBY_ASSERT_ALWAYS(!rb_objspace_garbage_object_p(handle));
|
|
RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(handle, imemo_ment));
|
|
}
|
|
|
|
// YJIT and ZJIT need this function to never allocate and never raise
|
|
VALUE
|
|
rb_yarv_ary_entry_internal(VALUE ary, long offset)
|
|
{
|
|
return rb_ary_entry_internal(ary, offset);
|
|
}
|
|
|
|
long
|
|
rb_jit_array_len(VALUE a)
|
|
{
|
|
return rb_array_len(a);
|
|
}
|
|
|
|
void
|
|
rb_set_cfp_pc(struct rb_control_frame_struct *cfp, const VALUE *pc)
|
|
{
|
|
cfp->pc = pc;
|
|
}
|
|
|
|
void
|
|
rb_set_cfp_sp(struct rb_control_frame_struct *cfp, VALUE *sp)
|
|
{
|
|
cfp->sp = sp;
|
|
}
|
|
|
|
bool
|
|
rb_jit_shape_too_complex_p(shape_id_t shape_id)
|
|
{
|
|
return rb_shape_too_complex_p(shape_id);
|
|
}
|
|
|
|
bool
|
|
rb_jit_multi_ractor_p(void)
|
|
{
|
|
return rb_multi_ractor_p();
|
|
}
|
|
|
|
// Acquire the VM lock and then signal all other Ruby threads (ractors) to
|
|
// contend for the VM lock, putting them to sleep. ZJIT and YJIT use this to
|
|
// evict threads running inside generated code so among other things, it can
|
|
// safely change memory protection of regions housing generated code.
|
|
void
|
|
rb_jit_vm_lock_then_barrier(unsigned int *recursive_lock_level, const char *file, int line)
|
|
{
|
|
rb_vm_lock_enter(recursive_lock_level, file, line);
|
|
rb_vm_barrier();
|
|
}
|
|
|
|
// Release the VM lock. The lock level must point to the same integer used to
|
|
// acquire the lock.
|
|
void
|
|
rb_jit_vm_unlock(unsigned int *recursive_lock_level, const char *file, int line)
|
|
{
|
|
rb_vm_lock_leave(recursive_lock_level, file, line);
|
|
}
|
|
|
|
void
|
|
rb_iseq_reset_jit_func(const rb_iseq_t *iseq)
|
|
{
|
|
RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq));
|
|
iseq->body->jit_entry = NULL;
|
|
iseq->body->jit_exception = NULL;
|
|
// Enable re-compiling this ISEQ. Event when it's invalidated for TracePoint,
|
|
// we'd like to re-compile ISEQs that haven't been converted to trace_* insns.
|
|
iseq->body->jit_entry_calls = 0;
|
|
iseq->body->jit_exception_calls = 0;
|
|
}
|
|
|
|
// Callback data for rb_jit_for_each_iseq
|
|
struct iseq_callback_data {
|
|
rb_iseq_callback callback;
|
|
void *data;
|
|
};
|
|
|
|
// Heap-walking callback for rb_jit_for_each_iseq
|
|
static int
|
|
for_each_iseq_i(void *vstart, void *vend, size_t stride, void *data)
|
|
{
|
|
const struct iseq_callback_data *callback_data = (struct iseq_callback_data *)data;
|
|
VALUE v = (VALUE)vstart;
|
|
for (; v != (VALUE)vend; v += stride) {
|
|
void *ptr = rb_asan_poisoned_object_p(v);
|
|
rb_asan_unpoison_object(v, false);
|
|
|
|
if (rb_obj_is_iseq(v)) {
|
|
rb_iseq_t *iseq = (rb_iseq_t *)v;
|
|
callback_data->callback(iseq, callback_data->data);
|
|
}
|
|
|
|
if (ptr) {
|
|
rb_asan_poison_object(v);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
uint32_t
|
|
rb_jit_get_page_size(void)
|
|
{
|
|
#if defined(_SC_PAGESIZE)
|
|
long page_size = sysconf(_SC_PAGESIZE);
|
|
if (page_size <= 0) rb_bug("jit: failed to get page size");
|
|
|
|
// 1 GiB limit. x86 CPUs with PDPE1GB can do this and anything larger is unexpected.
|
|
// Though our design sort of assume we have fine grained control over memory protection
|
|
// which require small page sizes.
|
|
if (page_size > 0x40000000l) rb_bug("jit page size too large");
|
|
|
|
return (uint32_t)page_size;
|
|
#else
|
|
#error "JIT supports POSIX only for now"
|
|
#endif
|
|
}
|
|
|
|
#if defined(MAP_FIXED_NOREPLACE) && defined(_SC_PAGESIZE)
|
|
// Align the current write position to a multiple of bytes
|
|
static uint8_t *
|
|
align_ptr(uint8_t *ptr, uint32_t multiple)
|
|
{
|
|
// Compute the pointer modulo the given alignment boundary
|
|
uint32_t rem = ((uint32_t)(uintptr_t)ptr) % multiple;
|
|
|
|
// If the pointer is already aligned, stop
|
|
if (rem == 0)
|
|
return ptr;
|
|
|
|
// Pad the pointer by the necessary amount to align it
|
|
uint32_t pad = multiple - rem;
|
|
|
|
return ptr + pad;
|
|
}
|
|
#endif
|
|
|
|
// Address space reservation. Memory pages are mapped on an as needed basis.
|
|
// See the Rust mm module for details.
|
|
uint8_t *
|
|
rb_jit_reserve_addr_space(uint32_t mem_size)
|
|
{
|
|
#ifndef _WIN32
|
|
uint8_t *mem_block;
|
|
|
|
// On Linux
|
|
#if defined(MAP_FIXED_NOREPLACE) && defined(_SC_PAGESIZE)
|
|
uint32_t const page_size = (uint32_t)sysconf(_SC_PAGESIZE);
|
|
uint8_t *const cfunc_sample_addr = (void *)(uintptr_t)&rb_jit_reserve_addr_space;
|
|
uint8_t *const probe_region_end = cfunc_sample_addr + INT32_MAX;
|
|
// Align the requested address to page size
|
|
uint8_t *req_addr = align_ptr(cfunc_sample_addr, page_size);
|
|
|
|
// Probe for addresses close to this function using MAP_FIXED_NOREPLACE
|
|
// to improve odds of being in range for 32-bit relative call instructions.
|
|
do {
|
|
mem_block = mmap(
|
|
req_addr,
|
|
mem_size,
|
|
PROT_NONE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE,
|
|
-1,
|
|
0
|
|
);
|
|
|
|
// If we succeeded, stop
|
|
if (mem_block != MAP_FAILED) {
|
|
ruby_annotate_mmap(mem_block, mem_size, "Ruby:rb_jit_reserve_addr_space");
|
|
break;
|
|
}
|
|
|
|
// -4MiB. Downwards to probe away from the heap. (On x86/A64 Linux
|
|
// main_code_addr < heap_addr, and in case we are in a shared
|
|
// library mapped higher than the heap, downwards is still better
|
|
// since it's towards the end of the heap rather than the stack.)
|
|
req_addr -= 4 * 1024 * 1024;
|
|
} while (req_addr < probe_region_end);
|
|
|
|
// On MacOS and other platforms
|
|
#else
|
|
// Try to map a chunk of memory as executable
|
|
mem_block = mmap(
|
|
(void *)rb_jit_reserve_addr_space,
|
|
mem_size,
|
|
PROT_NONE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS,
|
|
-1,
|
|
0
|
|
);
|
|
#endif
|
|
|
|
// Fallback
|
|
if (mem_block == MAP_FAILED) {
|
|
// Try again without the address hint (e.g., valgrind)
|
|
mem_block = mmap(
|
|
NULL,
|
|
mem_size,
|
|
PROT_NONE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS,
|
|
-1,
|
|
0
|
|
);
|
|
|
|
if (mem_block != MAP_FAILED) {
|
|
ruby_annotate_mmap(mem_block, mem_size, "Ruby:rb_jit_reserve_addr_space:fallback");
|
|
}
|
|
}
|
|
|
|
// Check that the memory mapping was successful
|
|
if (mem_block == MAP_FAILED) {
|
|
perror("ruby: jit: mmap:");
|
|
if(errno == ENOMEM) {
|
|
// No crash report if it's only insufficient memory
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
rb_bug("mmap failed");
|
|
}
|
|
|
|
return mem_block;
|
|
#else
|
|
// Windows not supported for now
|
|
return NULL;
|
|
#endif
|
|
}
|
|
|
|
// Walk all ISEQs in the heap and invoke the callback - shared between YJIT and ZJIT
|
|
void
|
|
rb_jit_for_each_iseq(rb_iseq_callback callback, void *data)
|
|
{
|
|
struct iseq_callback_data callback_data = { .callback = callback, .data = data };
|
|
rb_objspace_each_objects(for_each_iseq_i, (void *)&callback_data);
|
|
}
|
|
|
|
bool
|
|
rb_jit_mark_writable(void *mem_block, uint32_t mem_size)
|
|
{
|
|
return mprotect(mem_block, mem_size, PROT_READ | PROT_WRITE) == 0;
|
|
}
|
|
|
|
void
|
|
rb_jit_mark_executable(void *mem_block, uint32_t mem_size)
|
|
{
|
|
// Do not call mprotect when mem_size is zero. Some platforms may return
|
|
// an error for it. https://github.com/Shopify/ruby/issues/450
|
|
if (mem_size == 0) {
|
|
return;
|
|
}
|
|
if (mprotect(mem_block, mem_size, PROT_READ | PROT_EXEC)) {
|
|
rb_bug("Couldn't make JIT page (%p, %lu bytes) executable, errno: %s",
|
|
mem_block, (unsigned long)mem_size, strerror(errno));
|
|
}
|
|
}
|
|
|
|
// Free the specified memory block.
|
|
bool
|
|
rb_jit_mark_unused(void *mem_block, uint32_t mem_size)
|
|
{
|
|
// On Linux, you need to use madvise MADV_DONTNEED to free memory.
|
|
// We might not need to call this on macOS, but it's not really documented.
|
|
// We generally prefer to do the same thing on both to ease testing too.
|
|
madvise(mem_block, mem_size, MADV_DONTNEED);
|
|
|
|
// On macOS, mprotect PROT_NONE seems to reduce RSS.
|
|
// We also call this on Linux to avoid executing unused pages.
|
|
return mprotect(mem_block, mem_size, PROT_NONE) == 0;
|
|
}
|
|
|
|
// Invalidate icache for arm64.
|
|
// `start` is inclusive and `end` is exclusive.
|
|
void
|
|
rb_jit_icache_invalidate(void *start, void *end)
|
|
{
|
|
// Clear/invalidate the instruction cache. Compiles to nothing on x86_64
|
|
// but required on ARM before running freshly written code.
|
|
// On Darwin it's the same as calling sys_icache_invalidate().
|
|
#ifdef __GNUC__
|
|
__builtin___clear_cache(start, end);
|
|
#elif defined(__aarch64__)
|
|
#error No instruction cache clear available with this compiler on Aarch64!
|
|
#endif
|
|
}
|
|
|
|
VALUE
|
|
rb_jit_fix_mod_fix(VALUE recv, VALUE obj)
|
|
{
|
|
return rb_fix_mod_fix(recv, obj);
|
|
}
|
|
|
|
VALUE
|
|
rb_jit_fix_div_fix(VALUE recv, VALUE obj)
|
|
{
|
|
return rb_fix_div_fix(recv, obj);
|
|
}
|
|
|
|
// YJIT/ZJIT need this function to never allocate and never raise
|
|
VALUE
|
|
rb_yarv_str_eql_internal(VALUE str1, VALUE str2)
|
|
{
|
|
// We wrap this since it's static inline
|
|
return rb_str_eql_internal(str1, str2);
|
|
}
|
|
|
|
void rb_jit_str_concat_codepoint(VALUE str, VALUE codepoint);
|
|
|
|
attr_index_t
|
|
rb_jit_shape_capacity(shape_id_t shape_id)
|
|
{
|
|
return RSHAPE_CAPACITY(shape_id);
|
|
}
|