Skip to content

Commit

Permalink
Re-embed when removing Object instance variables
Browse files Browse the repository at this point in the history
Objects with the same shape must always have the same "embeddedness"
(either embedded or heap allocated) because YJIT assumes so. However,
using remove_instance_variable, it's possible that some objects are
embedded and some are heap allocated because it does not re-embed heap
allocated objects.

This commit changes remove_instance_variable to re-embed Object
instance variables when it becomes small enough.
  • Loading branch information
peterzhu2118 committed Dec 6, 2023
1 parent f80262b commit 12e3b07
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 6 deletions.
1 change: 1 addition & 0 deletions common.mk
Expand Up @@ -15849,6 +15849,7 @@ shape.$(OBJEXT): $(top_srcdir)/internal/compilers.h
shape.$(OBJEXT): $(top_srcdir)/internal/error.h
shape.$(OBJEXT): $(top_srcdir)/internal/gc.h
shape.$(OBJEXT): $(top_srcdir)/internal/imemo.h
shape.$(OBJEXT): $(top_srcdir)/internal/object.h
shape.$(OBJEXT): $(top_srcdir)/internal/serial.h
shape.$(OBJEXT): $(top_srcdir)/internal/static_assert.h
shape.$(OBJEXT): $(top_srcdir)/internal/string.h
Expand Down
6 changes: 0 additions & 6 deletions gc.c
Expand Up @@ -2960,12 +2960,6 @@ rb_newobj(void)
return newobj_of(GET_RACTOR(), 0, T_NONE, 0, 0, 0, FALSE, RVALUE_SIZE);
}

static size_t
rb_obj_embedded_size(uint32_t numiv)
{
return offsetof(struct RObject, as.ary) + (sizeof(VALUE) * numiv);
}

static VALUE
rb_class_instance_allocate_internal(VALUE klass, VALUE flags, bool wb_protected)
{
Expand Down
1 change: 1 addition & 0 deletions internal/object.h
Expand Up @@ -11,6 +11,7 @@
#include "ruby/ruby.h" /* for VALUE */

/* object.c */
size_t rb_obj_embedded_size(uint32_t numiv);
VALUE rb_class_search_ancestor(VALUE klass, VALUE super);
NORETURN(void rb_undefined_alloc(VALUE klass));
double rb_num_to_dbl(VALUE val);
Expand Down
6 changes: 6 additions & 0 deletions object.c
Expand Up @@ -92,6 +92,12 @@ static VALUE rb_cFalseClass_to_s;

/*! \endcond */

size_t
rb_obj_embedded_size(uint32_t numiv)
{
return offsetof(struct RObject, as.ary) + (sizeof(VALUE) * numiv);
}

VALUE
rb_obj_hide(VALUE obj)
{
Expand Down
12 changes: 12 additions & 0 deletions shape.c
Expand Up @@ -6,6 +6,7 @@
#include "internal/class.h"
#include "internal/error.h"
#include "internal/gc.h"
#include "internal/object.h"
#include "internal/symbol.h"
#include "internal/variable.h"
#include "variable.h"
Expand Down Expand Up @@ -642,6 +643,17 @@ rb_shape_transition_shape_remove_ivar(VALUE obj, ID id, rb_shape_t *shape, VALUE
memmove(&ivptr[removed_shape->next_iv_index - 1], &ivptr[removed_shape->next_iv_index],
((new_shape->next_iv_index + 1) - removed_shape->next_iv_index) * sizeof(VALUE));

// Re-embed objects when instances become small enough
// This is necessary because YJIT assumes that objects with the same shape
// have the same embeddedness for efficiency (avoid extra checks)
if (BUILTIN_TYPE(obj) == T_OBJECT &&
!RB_FL_TEST_RAW(obj, ROBJECT_EMBED) &&
rb_obj_embedded_size(new_shape->next_iv_index) <= rb_gc_obj_slot_size(obj)) {
RB_FL_SET_RAW(obj, ROBJECT_EMBED);
memcpy(ROBJECT_IVPTR(obj), ivptr, new_shape->next_iv_index * sizeof(VALUE));
xfree(ivptr);
}

rb_shape_set_shape(obj, new_shape);
}
return true;
Expand Down
35 changes: 35 additions & 0 deletions test/ruby/test_object.rb
Expand Up @@ -355,6 +355,41 @@ def test_remove_instance_variable
end
end

def test_remove_instance_variable_re_embed
require "objspace"

c = Class.new do
def a = @a

def b = @b

def c = @c
end

o1 = c.new
o2 = c.new

o1.instance_variable_set(:@foo, 5)
o1.instance_variable_set(:@a, 0)
o1.instance_variable_set(:@b, 1)
o1.instance_variable_set(:@c, 2)
refute_includes ObjectSpace.dump(o1), '"embedded":true'
o1.remove_instance_variable(:@foo)
assert_includes ObjectSpace.dump(o1), '"embedded":true'

o2.instance_variable_set(:@a, 0)
o2.instance_variable_set(:@b, 1)
o2.instance_variable_set(:@c, 2)
assert_includes ObjectSpace.dump(o2), '"embedded":true'

assert_equal(0, o1.a)
assert_equal(1, o1.b)
assert_equal(2, o1.c)
assert_equal(0, o2.a)
assert_equal(1, o2.b)
assert_equal(2, o2.c)
end

def test_convert_string
o = Object.new
def o.to_s; 1; end
Expand Down

0 comments on commit 12e3b07

Please sign in to comment.