YJIT: getinstancevariable cache indexes for types other than T_OBJECT

While accessing the ivars of other types is too complicated to
realistically generate the ASM for it, we can at least provide
the ivar index as to not have to lookup the shape tree every
time.

```
compare-ruby: ruby 3.5.0dev (2025-08-27T14:58:58Z merge-vm-setivar-d.. 5b749d8e53) +YJIT +PRISM [arm64-darwin24]
built-ruby: ruby 3.5.0dev (2025-08-28T17:58:32Z yjit-get-exivar efaa8c9b09) +YJIT +PRISM [arm64-darwin24]

|                           |compare-ruby|built-ruby|
|:--------------------------|-----------:|---------:|
|vm_ivar_get_on_obj         |     930.458|   936.865|
|                           |           -|     1.01x|
|vm_ivar_get_on_class       |     134.471|   431.622|
|                           |           -|     3.21x|
|vm_ivar_get_on_generic     |     146.679|   284.408|
|                           |           -|     1.94x|
```

Co-Authored-By: Aaron Patterson <tenderlove@ruby-lang.org>
This commit is contained in:
Jean Boussier 2025-08-28 19:29:46 +02:00
parent b1dbcd3ce3
commit b6d4882c05
7 changed files with 123 additions and 24 deletions

View File

@ -1,17 +1,75 @@
prelude: |
class Example
def initialize
@levar = 1
@v0 = 1
@v1 = 2
@v3 = 3
@levar = 1
end
def get_value_loop
sum = 0
i = 0
while i < 1000000
while i < 100_000
# 10 times to de-emphasize loop overhead
sum += @levar
sum += @levar
sum += @levar
sum += @levar
sum += @levar
sum += @levar
sum += @levar
sum += @levar
sum += @levar
sum += @levar
i += 1
end
return sum
end
@levar = 1
@v0 = 1
@v1 = 2
@v3 = 3
def self.get_value_loop
sum = 0
i = 0
while i < 100_000
# 10 times to de-emphasize loop overhead
sum += @levar
sum += @levar
sum += @levar
sum += @levar
sum += @levar
sum += @levar
sum += @levar
sum += @levar
sum += @levar
sum += @levar
i += 1
end
return sum
end
end
class GenExample < Time
def initialize
@levar = 1
@v0 = 1
@v1 = 2
@v3 = 3
end
def get_value_loop
sum = 0
i = 0
while i < 100_000
# 10 times to de-emphasize loop overhead
sum += @levar
sum += @levar
@ -31,7 +89,12 @@ prelude: |
end
obj = Example.new
gen = GenExample.new
benchmark:
vm_ivar_get: |
vm_ivar_get_on_obj: |
obj.get_value_loop
vm_ivar_get_on_class: |
Example.get_value_loop
vm_ivar_get_on_generic: |
gen.get_value_loop
loop_count: 100

View File

@ -53,6 +53,7 @@ VALUE rb_obj_field_get(VALUE obj, shape_id_t target_shape_id);
void rb_ivar_set_internal(VALUE obj, ID id, VALUE val);
attr_index_t rb_ivar_set_index(VALUE obj, ID id, VALUE val);
attr_index_t rb_obj_field_set(VALUE obj, shape_id_t target_shape_id, ID field_name, VALUE val);
VALUE rb_ivar_get_at(VALUE obj, attr_index_t index, ID id);
RUBY_SYMBOL_EXPORT_BEGIN
/* variable.c (export) */

View File

@ -24,6 +24,7 @@ STATIC_ASSERT(shape_id_num_bits, SHAPE_ID_NUM_BITS == sizeof(shape_id_t) * CHAR_
// index in rb_shape_tree.shape_list. Allow to access `rb_shape_t *`.
// 19-21 SHAPE_ID_HEAP_INDEX_MASK
// index in rb_shape_tree.capacities. Allow to access slot size.
// Always 0 except for T_OBJECT.
// 22 SHAPE_ID_FL_FROZEN
// Whether the object is frozen or not.
// 23 SHAPE_ID_FL_HAS_OBJECT_ID

View File

@ -1463,6 +1463,36 @@ rb_ivar_get(VALUE obj, ID id)
return iv;
}
VALUE
rb_ivar_get_at(VALUE obj, attr_index_t index, ID id)
{
RUBY_ASSERT(rb_is_instance_id(id));
// Used by JITs, but never for T_OBJECT.
switch (BUILTIN_TYPE(obj)) {
case T_OBJECT:
UNREACHABLE_RETURN(Qundef);
case T_CLASS:
case T_MODULE:
{
VALUE fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(obj);
VALUE val = rb_imemo_fields_ptr(fields_obj)[index];
if (UNLIKELY(!rb_ractor_main_p()) && !rb_ractor_shareable_p(val)) {
rb_raise(rb_eRactorIsolationError,
"can not get unshareable values from instance variables of classes/modules from non-main Ractors");
}
return val;
}
default:
{
VALUE fields_obj = rb_obj_fields(obj, id);
return rb_imemo_fields_ptr(fields_obj)[index];
}
}
}
VALUE
rb_attr_get(VALUE obj, ID id)
{

View File

@ -313,6 +313,7 @@ fn main() {
// From yjit.c
.allowlist_function("rb_object_shape_count")
.allowlist_function("rb_ivar_get_at")
.allowlist_function("rb_iseq_(get|set)_yjit_payload")
.allowlist_function("rb_iseq_pc_at_idx")
.allowlist_function("rb_iseq_opcode_at_pc")

View File

@ -2864,7 +2864,7 @@ fn gen_get_ivar(
// NOTE: This assumes T_OBJECT can't ever have the same shape_id as any other type.
// too-complex shapes can't use index access, so we use rb_ivar_get for them too.
if !receiver_t_object || comptime_receiver.shape_too_complex() || megamorphic {
if !comptime_receiver.heap_object_p() || comptime_receiver.shape_too_complex() || megamorphic {
// General case. Call rb_ivar_get().
// VALUE rb_ivar_get(VALUE obj, ID id)
asm_comment!(asm, "call rb_ivar_get()");
@ -2900,9 +2900,6 @@ fn gen_get_ivar(
// Guard heap object (recv_opnd must be used before stack_pop)
guard_object_is_heap(asm, recv, recv_opnd, Counter::getivar_not_heap);
// Compile time self is embedded and the ivar index lands within the object
let embed_test_result = comptime_receiver.embedded_p();
let expected_shape = unsafe { rb_obj_shape_id(comptime_receiver) };
let shape_id_offset = unsafe { rb_shape_id_offset() };
let shape_opnd = Opnd::mem(SHAPE_ID_NUM_BITS as u8, recv, shape_id_offset);
@ -2931,28 +2928,33 @@ fn gen_get_ivar(
asm.mov(out_opnd, Qnil.into());
}
Some(ivar_index) => {
if embed_test_result {
// See ROBJECT_FIELDS() from include/ruby/internal/core/robject.h
let ivar_opnd = if receiver_t_object {
if comptime_receiver.embedded_p() {
// See ROBJECT_FIELDS() from include/ruby/internal/core/robject.h
// Load the variable
let offs = ROBJECT_OFFSET_AS_ARY as i32 + (ivar_index * SIZEOF_VALUE) as i32;
let ivar_opnd = Opnd::mem(64, recv, offs);
// Load the variable
let offs = ROBJECT_OFFSET_AS_ARY as i32 + (ivar_index * SIZEOF_VALUE) as i32;
Opnd::mem(64, recv, offs)
} else {
// Compile time value is *not* embedded.
// Push the ivar on the stack
let out_opnd = asm.stack_push(Type::Unknown);
asm.mov(out_opnd, ivar_opnd);
// Get a pointer to the extended table
let tbl_opnd = asm.load(Opnd::mem(64, recv, ROBJECT_OFFSET_AS_HEAP_FIELDS as i32));
// Read the ivar from the extended table
Opnd::mem(64, tbl_opnd, (SIZEOF_VALUE * ivar_index) as i32)
}
} else {
// Compile time value is *not* embedded.
asm_comment!(asm, "call rb_ivar_get_at()");
// Get a pointer to the extended table
let tbl_opnd = asm.load(Opnd::mem(64, recv, ROBJECT_OFFSET_AS_HEAP_FIELDS as i32));
// The function could raise RactorIsolationError.
jit_prepare_non_leaf_call(jit, asm);
asm.ccall(rb_ivar_get_at as *const u8, vec![recv, Opnd::UImm((ivar_index as u32).into()), Opnd::UImm(ivar_name)])
};
// Read the ivar from the extended table
let ivar_opnd = Opnd::mem(64, tbl_opnd, (SIZEOF_VALUE * ivar_index) as i32);
let out_opnd = asm.stack_push(Type::Unknown);
asm.mov(out_opnd, ivar_opnd);
}
// Push the ivar on the stack
let out_opnd = asm.stack_push(Type::Unknown);
asm.mov(out_opnd, ivar_opnd);
}
}

View File

@ -1115,6 +1115,7 @@ extern "C" {
pub fn rb_obj_shape_id(obj: VALUE) -> shape_id_t;
pub fn rb_shape_get_iv_index(shape_id: shape_id_t, id: ID, value: *mut attr_index_t) -> bool;
pub fn rb_shape_transition_add_ivar_no_warnings(obj: VALUE, id: ID) -> shape_id_t;
pub fn rb_ivar_get_at(obj: VALUE, index: attr_index_t, id: ID) -> VALUE;
pub fn rb_gvar_get(arg1: ID) -> VALUE;
pub fn rb_gvar_set(arg1: ID, arg2: VALUE) -> VALUE;
pub fn rb_ensure_iv_list_size(obj: VALUE, current_len: u32, newsize: u32);