Skip to content

Commit c57d594

Browse files
authored
ZJIT: Use LoadField for Class/Module ivars (ruby#16252)
Assume only one box (root box) and invalidate otherwise. Drops C calls to `rb_ivar_get_at_no_ractor_check`. Before: ``` Top-20 calls to C functions from JIT code (77.3% of total 64,311,573): rb_vm_opt_send_without_block: 11,939,854 (18.6%) rb_hash_aref: 5,400,091 ( 8.4%) rb_vm_invokeblock: 4,453,357 ( 6.9%) rb_zjit_writebarrier_check_immediate: 4,279,890 ( 6.7%) rb_vm_getinstancevariable: 3,504,908 ( 5.4%) rb_vm_send: 3,103,424 ( 4.8%) rb_ivar_get_at_no_ractor_check: 2,864,766 ( 4.5%) rb_obj_is_kind_of: 2,313,479 ( 3.6%) rb_hash_aset: 1,903,359 ( 3.0%) Hash#fetch: 1,639,937 ( 2.5%) rb_vm_setinstancevariable: 1,596,791 ( 2.5%) rb_vm_opt_getconstant_path: 1,328,761 ( 2.1%) rb_jit_ary_push: 960,563 ( 1.5%) rb_ec_ary_new_from_values: 722,913 ( 1.1%) rb_class_allocate_instance: 721,483 ( 1.1%) fetch: 713,134 ( 1.1%) rb_str_buf_append: 667,545 ( 1.0%) rb_ivar_get: 585,817 ( 0.9%) rb_hash_new_with_size: 520,347 ( 0.8%) rb_vm_sendforward: 479,029 ( 0.7%) ``` After: ``` Top-20 calls to C functions from JIT code (76.5% of total 62,282,359): rb_vm_opt_send_without_block: 11,939,850 (19.2%) rb_hash_aref: 5,400,092 ( 8.7%) rb_vm_invokeblock: 4,453,357 ( 7.2%) rb_zjit_writebarrier_check_immediate: 4,279,893 ( 6.9%) rb_vm_getinstancevariable: 3,504,920 ( 5.6%) rb_vm_send: 3,103,441 ( 5.0%) rb_obj_is_kind_of: 2,313,510 ( 3.7%) rb_hash_aset: 1,903,359 ( 3.1%) Hash#fetch: 1,639,937 ( 2.6%) rb_vm_setinstancevariable: 1,596,797 ( 2.6%) rb_vm_opt_getconstant_path: 1,328,761 ( 2.1%) rb_jit_ary_push: 960,563 ( 1.5%) rb_ivar_get_at_no_ractor_check: 835,498 ( 1.3%) rb_ec_ary_new_from_values: 722,921 ( 1.2%) rb_class_allocate_instance: 721,492 ( 1.2%) fetch: 713,135 ( 1.1%) rb_str_buf_append: 667,545 ( 1.1%) rb_ivar_get: 585,815 ( 0.9%) rb_hash_new_with_size: 520,348 ( 0.8%) rb_vm_sendforward: 479,029 ( 0.8%) ``` The remaining `rb_ivar_get_at_no_ractor_check` are due to TypedData access, mostly on `Thread`.
1 parent 50f51f7 commit c57d594

14 files changed

Lines changed: 237 additions & 11 deletions

File tree

box.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "ruby/util.h"
1919
#include "vm_core.h"
2020
#include "darray.h"
21+
#include "zjit.h"
2122

2223
#include <stdio.h>
2324

@@ -390,6 +391,9 @@ box_initialize(VALUE box_value)
390391

391392
rb_ivar_set(box_value, id_box_entry, entry);
392393

394+
// Invalidate ZJIT code that assumes only the root box is active
395+
rb_zjit_invalidate_root_box();
396+
393397
return box_value;
394398
}
395399

depend

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,7 @@ box.$(OBJEXT): {$(VPATH)}vm_core.h
980980
box.$(OBJEXT): {$(VPATH)}vm_debug.h
981981
box.$(OBJEXT): {$(VPATH)}vm_opts.h
982982
box.$(OBJEXT): {$(VPATH)}vm_sync.h
983+
box.$(OBJEXT): {$(VPATH)}zjit.h
983984
builtin.$(OBJEXT): $(CCAN_DIR)/check_type/check_type.h
984985
builtin.$(OBJEXT): $(CCAN_DIR)/container_of/container_of.h
985986
builtin.$(OBJEXT): $(CCAN_DIR)/list/list.h

jit.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
#include "vm_sync.h"
1717
#include "internal/fixnum.h"
1818
#include "internal/string.h"
19+
#include "internal/class.h"
20+
#include "internal/imemo.h"
1921

2022
enum jit_bindgen_constants {
2123
// Field offsets for the RObject struct
2224
ROBJECT_OFFSET_AS_HEAP_FIELDS = offsetof(struct RObject, as.heap.fields),
2325
ROBJECT_OFFSET_AS_ARY = offsetof(struct RObject, as.ary),
2426

27+
// Field offset for prime classext's fields_obj from a class pointer
28+
RCLASS_OFFSET_PRIME_FIELDS_OBJ = offsetof(struct RClass_and_rb_classext_t, classext.fields_obj),
29+
2530
// Field offsets for the RString struct
2631
RUBY_OFFSET_RSTRING_LEN = offsetof(struct RString, len),
2732

@@ -529,6 +534,13 @@ rb_jit_multi_ractor_p(void)
529534
return rb_multi_ractor_p();
530535
}
531536

537+
bool
538+
rb_jit_class_fields_embedded_p(VALUE klass)
539+
{
540+
VALUE fields_obj = RCLASS_EXT_PRIME(klass)->fields_obj;
541+
return !fields_obj || !FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP);
542+
}
543+
532544
// Acquire the VM lock and then signal all other Ruby threads (ractors) to
533545
// contend for the VM lock, putting them to sleep. ZJIT and YJIT use this to
534546
// evict threads running inside generated code so among other things, it can

yjit/src/cruby_bindings.inc.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

zjit.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ void rb_zjit_iseq_free(const rb_iseq_t *iseq);
2828
void rb_zjit_before_ractor_spawn(void);
2929
void rb_zjit_tracing_invalidate_all(void);
3030
void rb_zjit_invalidate_no_singleton_class(VALUE klass);
31+
void rb_zjit_invalidate_root_box(void);
3132
#else
3233
#define rb_zjit_entry 0
3334
static inline void rb_zjit_compile_iseq(const rb_iseq_t *iseq, bool jit_exception) {}
@@ -40,6 +41,7 @@ static inline void rb_zjit_constant_state_changed(ID id) {}
4041
static inline void rb_zjit_before_ractor_spawn(void) {}
4142
static inline void rb_zjit_tracing_invalidate_all(void) {}
4243
static inline void rb_zjit_invalidate_no_singleton_class(VALUE klass) {}
44+
static inline void rb_zjit_invalidate_root_box(void) {}
4345
#endif // #if USE_ZJIT
4446

4547
#define rb_zjit_enabled_p (rb_zjit_entry != 0)

zjit/bindgen/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ fn main() {
309309
.allowlist_function("rb_assert_holding_vm_lock")
310310
.allowlist_function("rb_jit_shape_too_complex_p")
311311
.allowlist_function("rb_jit_multi_ractor_p")
312+
.allowlist_function("rb_jit_class_fields_embedded_p")
312313
.allowlist_function("rb_jit_vm_lock_then_barrier")
313314
.allowlist_function("rb_jit_vm_unlock")
314315
.allowlist_function("rb_jit_for_each_iseq")

zjit/src/codegen.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use std::slice;
1010
use crate::backend::current::ALLOC_REGS;
1111
use crate::invariants::{
1212
track_bop_assumption, track_cme_assumption, track_no_ep_escape_assumption, track_no_trace_point_assumption,
13-
track_single_ractor_assumption, track_stable_constant_names_assumption, track_no_singleton_class_assumption
13+
track_single_ractor_assumption, track_stable_constant_names_assumption, track_no_singleton_class_assumption,
14+
track_root_box_assumption
1415
};
1516
use crate::gc::append_gc_offsets;
1617
use crate::payload::{get_or_create_iseq_payload, IseqCodePtrs, IseqVersion, IseqVersionRef, IseqStatus};
@@ -874,6 +875,9 @@ pub fn split_patch_point(asm: &mut Assembler, target: &Target, invariant: Invari
874875
Invariant::NoSingletonClass { klass } => {
875876
track_no_singleton_class_assumption(klass, code_ptr, side_exit_ptr, version);
876877
}
878+
Invariant::RootBoxOnly => {
879+
track_root_box_assumption(code_ptr, side_exit_ptr, version);
880+
}
877881
}
878882
});
879883
}

zjit/src/cruby.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,10 @@ impl VALUE {
581581
}
582582
}
583583

584+
pub fn class_fields_embedded_p(self) -> bool {
585+
unsafe { rb_jit_class_fields_embedded_p(self) }
586+
}
587+
584588
pub fn as_fixnum(self) -> i64 {
585589
assert!(self.fixnum_p());
586590
(self.0 as i64) >> 1
@@ -1491,6 +1495,7 @@ pub(crate) mod ids {
14911495
name: aref content: b"[]"
14921496
name: len
14931497
name: _as_heap
1498+
name: _fields_obj
14941499
name: thread_ptr
14951500
name: self_ content: b"self"
14961501
name: rb_ivar_get_at_no_ractor_check

zjit/src/cruby_bindings.inc.rs

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

zjit/src/hir.rs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#![allow(clippy::match_like_matches_macro)]
88
use crate::{
99
backend::lir::C_ARG_OPNDS,
10-
cast::IntoUsize, codegen::local_idx_to_ep_offset, cruby::*, invariants::has_singleton_class_of, payload::{get_or_create_iseq_payload, IseqPayload}, options::{debug, get_option, DumpHIR}, state::ZJITState, json::Json
10+
cast::IntoUsize, codegen::local_idx_to_ep_offset, cruby::*, invariants::{self, has_singleton_class_of}, payload::{get_or_create_iseq_payload, IseqPayload}, options::{debug, get_option, DumpHIR}, state::ZJITState, json::Json
1111
};
1212
use std::{
1313
cell::RefCell, collections::{BTreeSet, HashMap, HashSet, VecDeque}, ffi::{c_void, c_uint, c_int, CStr}, fmt::Display, mem::{align_of, size_of}, ptr, slice::Iter
@@ -151,6 +151,9 @@ pub enum Invariant {
151151
NoSingletonClass {
152152
klass: VALUE,
153153
},
154+
/// Only the root box is active, so we can safely read from the prime classext.
155+
/// Invalidated if a non-root box duplicates any classext.
156+
RootBoxOnly,
154157
}
155158

156159
impl Invariant {
@@ -290,6 +293,7 @@ impl<'a> std::fmt::Display for InvariantPrinter<'a> {
290293
class_name,
291294
self.ptr_map.map_ptr(klass.as_ptr::<VALUE>()))
292295
}
296+
Invariant::RootBoxOnly => write!(f, "RootBoxOnly"),
293297
}
294298
}
295299
}
@@ -2182,6 +2186,17 @@ impl Function {
21822186
}
21832187
}
21842188

2189+
/// Assume that only the root box is active, so we can safely read from the prime classext.
2190+
/// Returns true if safe to assume so and emits a PatchPoint.
2191+
pub fn assume_root_box(&mut self, block: BlockId, state: InsnId) -> bool {
2192+
if invariants::non_root_box_created() {
2193+
false
2194+
} else {
2195+
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::RootBoxOnly, state });
2196+
true
2197+
}
2198+
}
2199+
21852200
/// Assume that objects of a given class will have no singleton class.
21862201
/// Returns true if safe to assume so and emits a PatchPoint.
21872202
/// Returns false if we've already seen a singleton class for this class,
@@ -3899,10 +3914,52 @@ impl Function {
38993914
// entered the compiler. That means we can just return nil for this
39003915
// shape + iv name
39013916
self.push_insn(block, Insn::Const { val: Const::Value(Qnil) })
3917+
} else if recv_type.flags().is_t_class_or_module() {
3918+
// Class/module ivar: load from prime classext's fields_obj
3919+
if self.assume_root_box(block, state) {
3920+
// Root box only: load directly from prime classext
3921+
let fields_obj = self.push_insn(block, Insn::LoadField {
3922+
recv: self_val, id: ID!(_fields_obj),
3923+
offset: RCLASS_OFFSET_PRIME_FIELDS_OBJ as i32,
3924+
return_type: types::RubyValue,
3925+
});
3926+
if recv_type.flags().is_fields_embedded() {
3927+
let offset = ROBJECT_OFFSET_AS_ARY as i32
3928+
+ (SIZEOF_VALUE * ivar_index.to_usize()) as i32;
3929+
self.push_insn(block, Insn::LoadField {
3930+
recv: fields_obj, id, offset,
3931+
return_type: types::BasicObject,
3932+
})
3933+
} else {
3934+
let ptr = self.push_insn(block, Insn::LoadField {
3935+
recv: fields_obj, id: ID!(_as_heap),
3936+
offset: ROBJECT_OFFSET_AS_HEAP_FIELDS as i32,
3937+
return_type: types::CPtr,
3938+
});
3939+
let offset = SIZEOF_VALUE_I32 * ivar_index as i32;
3940+
self.push_insn(block, Insn::LoadField {
3941+
recv: ptr, id, offset,
3942+
return_type: types::BasicObject,
3943+
})
3944+
}
3945+
} else {
3946+
// Non-root box active: fall back to C call
3947+
// NOTE: it's fine to use rb_ivar_get_at_no_ractor_check because
3948+
// getinstancevariable does assume_single_ractor_mode()
3949+
let ivar_index_insn = self.push_insn(block, Insn::Const { val: Const::CUInt16(ivar_index as u16) });
3950+
self.push_insn(block, Insn::CCall {
3951+
cfunc: rb_ivar_get_at_no_ractor_check as *const u8,
3952+
recv: self_val,
3953+
args: vec![ivar_index_insn],
3954+
name: ID!(rb_ivar_get_at_no_ractor_check),
3955+
return_type: types::BasicObject,
3956+
elidable: true })
3957+
}
39023958
} else if !recv_type.flags().is_t_object() {
3959+
// Non-T_OBJECT, non-class/module (e.g. T_DATA): fall back to C call
39033960
// NOTE: it's fine to use rb_ivar_get_at_no_ractor_check because
39043961
// getinstancevariable does assume_single_ractor_mode()
3905-
let ivar_index_insn: InsnId = self.push_insn(block, Insn::Const { val: Const::CUInt16(ivar_index as u16) });
3962+
let ivar_index_insn = self.push_insn(block, Insn::Const { val: Const::CUInt16(ivar_index as u16) });
39063963
self.push_insn(block, Insn::CCall {
39073964
cfunc: rb_ivar_get_at_no_ractor_check as *const u8,
39083965
recv: self_val,

0 commit comments

Comments
 (0)