variable.c: add object shapes for MRB_TT_OBJECT IV storage

Introduce "object shapes" (hidden classes) that share IV key
layouts across objects with the same instance variable assignment
order. This eliminates per-object key storage overhead.

Memory savings: ~22% heap reduction for object-heavy workloads
(e.g., 150k objects with 2-6 IVs). Per-object: 40->24 bytes
for 2 IVs. Objects exceeding 16 IVs or using
remove_instance_variable fall back to traditional iv_tbl.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-02-27 08:29:46 +09:00
parent b4d2524fb4
commit 8d10056aff
6 changed files with 404 additions and 13 deletions
+2
View File
@@ -325,6 +325,8 @@ typedef struct mrb_state {
struct mrb_mt_rom_list *rom_mt; /* heap-allocated ROM wrappers (freed at close) */
struct mrb_iv_shape *root_shape; /* root of IV shape tree */
void *ud; /* auxiliary data */
#ifdef MRB_FIXED_STATE_ATEXIT_STACK
+4
View File
@@ -199,6 +199,10 @@ void mrb_gc_free_gv(mrb_state*);
size_t mrb_gc_mark_iv(mrb_state*, struct RObject*);
void mrb_gc_free_iv(mrb_state*, struct RObject*);
/* IV shape tree */
void mrb_init_shape(mrb_state*);
void mrb_free_shape(mrb_state*);
/* VM */
#define MRB_CI_VISIBILITY(ci) MRB_FLAGS_GET((ci)->vis, 0, 2)
#define MRB_CI_SET_VISIBILITY(ci, visi) MRB_FLAGS_SET((ci)->vis, 0, 2, visi)
+5
View File
@@ -24,6 +24,11 @@ struct RBasic {
#define MRB_OBJ_IS_FROZEN 1
#define mrb_frozen_p(o) ((o)->frozen)
/* Object shape flag -- when set, obj->iv is shaped, not iv_tbl* */
/* Bit 5: avoids conflict with MRB_INSTANCE_TT_MASK (bits 0-4) */
#define MRB_FL_OBJ_SHAPED (1 << 5)
#define MRB_OBJ_SHAPED_P(o) ((o)->flags & MRB_FL_OBJ_SHAPED)
struct RObject {
MRB_OBJECT_HEADER;
struct iv_tbl *iv;
+3
View File
@@ -601,6 +601,9 @@ mrb_obj_alloc(mrb_state *mrb, enum mrb_vtype ttype, struct RClass *cls)
*p = RVALUE_zero;
p->as.basic.tt = ttype;
p->as.basic.c = cls;
if (ttype == MRB_TT_OBJECT) {
p->as.basic.flags |= MRB_FL_OBJ_SHAPED;
}
paint_partial_white(gc, &p->as.basic);
return &p->as.basic;
}
+5
View File
@@ -22,12 +22,16 @@ void mrb_gc_destroy(mrb_state*, mrb_gc *gc);
int mrb_core_init_protect(mrb_state *mrb, void (*body)(mrb_state*, void*), void *opaque);
void mrb_init_shape(mrb_state*);
void mrb_free_shape(mrb_state*);
static void
init_gc_and_core(mrb_state *mrb, void *opaque)
{
static const struct mrb_context mrb_context_zero = { 0 };
mrb_gc_init(mrb, &mrb->gc);
mrb_init_shape(mrb);
mrb->c = (struct mrb_context*)mrb_malloc(mrb, sizeof(struct mrb_context));
*mrb->c = mrb_context_zero;
mrb->root_c = mrb->c;
@@ -187,6 +191,7 @@ mrb_close(mrb_state *mrb)
/* free */
mrb_gc_free_gv(mrb);
mrb_gc_destroy(mrb, &mrb->gc);
mrb_free_shape(mrb);
mrb_free_context(mrb, mrb->root_c);
mrb_free_symtbl(mrb);
+385 -13
View File
@@ -239,6 +239,287 @@ iv_free(mrb_state *mrb, iv_tbl *t)
mrb_free(mrb, t);
}
/*
* Object Shape (Hidden Class) structures.
*
* A shape describes the IV layout of an object: which syms are stored
* at which indices. Shapes form a tree rooted at the empty root shape.
* Each child adds one IV (its "edge" sym). Objects sharing the same
* set of IVs (assigned in the same order) share the same shape,
* eliminating per-object key storage.
*
* Only MRB_TT_OBJECT instances are shaped. RClass, RHash, etc. keep
* traditional iv_tbl.
*/
/* Maximum IV count before de-shaping to iv_tbl */
#define MRB_SHAPE_MAX_IVS 16
/* Shape descriptor -- shared across objects with same IV layout */
typedef struct mrb_iv_shape {
struct mrb_iv_shape *parent; /* parent shape (one fewer IV) */
struct mrb_iv_shape *children; /* linked list of child shapes */
struct mrb_iv_shape *sibling; /* next child of same parent */
mrb_sym edge; /* IV sym added from parent */
uint16_t count; /* number of IV slots */
} mrb_iv_shape;
/* Per-object shaped IV storage (allocated via struct hack) */
typedef struct mrb_shaped_iv {
mrb_iv_shape *shape;
mrb_value values[1]; /* shape->count elements */
} mrb_shaped_iv;
/* Create the empty root shape */
static mrb_iv_shape*
shape_root(mrb_state *mrb)
{
mrb_iv_shape *s = (mrb_iv_shape*)mrb_calloc(mrb, 1, sizeof(mrb_iv_shape));
return s;
}
/* Find a child shape with the given edge sym */
static mrb_iv_shape*
shape_find_child(mrb_iv_shape *shape, mrb_sym sym)
{
mrb_iv_shape *c = shape->children;
while (c) {
if (c->edge == sym) return c;
c = c->sibling;
}
return NULL;
}
/* Find or create a child shape for adding sym */
static mrb_iv_shape*
shape_transition(mrb_state *mrb, mrb_iv_shape *shape, mrb_sym sym)
{
mrb_iv_shape *child = shape_find_child(shape, sym);
if (child) return child;
/* create new child shape */
child = (mrb_iv_shape*)mrb_malloc(mrb, sizeof(mrb_iv_shape));
child->parent = shape;
child->children = NULL;
child->sibling = shape->children;
child->edge = sym;
child->count = shape->count + 1;
shape->children = child;
return child;
}
/*
* Look up sym in shape by walking the parent chain.
* Returns the value index (0-based), or -1 if not found.
*/
static int
shape_lookup(mrb_iv_shape *shape, mrb_sym sym)
{
mrb_iv_shape *s = shape;
while (s->count > 0) {
if (s->edge == sym) return s->count - 1;
s = s->parent;
}
return -1;
}
/* Recursively free all shapes in the tree */
static void
shape_free_tree(mrb_state *mrb, mrb_iv_shape *shape)
{
mrb_iv_shape *c = shape->children;
while (c) {
mrb_iv_shape *next = c->sibling;
shape_free_tree(mrb, c);
c = next;
}
mrb_free(mrb, shape);
}
/* Allocate a mrb_shaped_iv with room for count values */
static mrb_shaped_iv*
shaped_iv_alloc(mrb_state *mrb, mrb_iv_shape *shape)
{
size_t sz = offsetof(mrb_shaped_iv, values) +
sizeof(mrb_value) * shape->count;
mrb_shaped_iv *siv = (mrb_shaped_iv*)mrb_malloc(mrb, sz);
siv->shape = shape;
return siv;
}
/* Convert a shaped object back to traditional iv_tbl (de-shape) */
static void
shaped_to_iv_tbl(mrb_state *mrb, struct RObject *obj)
{
mrb_shaped_iv *siv = (mrb_shaped_iv*)obj->iv;
iv_tbl *t = NULL;
if (siv) {
mrb_iv_shape *shape = siv->shape;
if (shape->count > 0) {
/* reconstruct keys from parent chain */
mrb_sym keys[MRB_SHAPE_MAX_IVS];
mrb_iv_shape *s = shape;
while (s->count > 0) {
keys[s->count - 1] = s->edge;
s = s->parent;
}
t = iv_new(mrb);
for (int i = 0; i < shape->count; i++) {
if (!mrb_undef_p(siv->values[i])) {
iv_put(mrb, t, keys[i], siv->values[i]);
}
}
}
mrb_free(mrb, siv);
}
obj->iv = t;
obj->flags &= ~MRB_FL_OBJ_SHAPED;
}
/* --- Shaped IV operations --- */
static void
shaped_iv_set(mrb_state *mrb, struct RObject *obj, mrb_sym sym, mrb_value v)
{
mrb_shaped_iv *siv = (mrb_shaped_iv*)obj->iv;
mrb_iv_shape *shape = siv ? siv->shape : mrb->root_shape;
/* check if sym already exists in current shape */
int idx = shape_lookup(shape, sym);
if (idx >= 0) {
siv->values[idx] = v;
return;
}
/* transition to new shape */
mrb_iv_shape *new_shape = shape_transition(mrb, shape, sym);
/* de-shape if too many IVs */
if (new_shape->count > MRB_SHAPE_MAX_IVS) {
shaped_to_iv_tbl(mrb, obj);
if (!obj->iv) {
obj->iv = iv_new(mrb);
}
iv_put(mrb, obj->iv, sym, v);
return;
}
/* allocate new shaped_iv with room for new shape */
mrb_shaped_iv *new_siv = shaped_iv_alloc(mrb, new_shape);
/* copy old values (they are a prefix) */
if (siv) {
memcpy(new_siv->values, siv->values,
sizeof(mrb_value) * shape->count);
mrb_free(mrb, siv);
}
/* new IV goes in the last slot */
new_siv->values[new_shape->count - 1] = v;
obj->iv = (iv_tbl*)new_siv;
}
static mrb_value
shaped_iv_get(struct RObject *obj, mrb_sym sym)
{
mrb_shaped_iv *siv = (mrb_shaped_iv*)obj->iv;
if (!siv) return mrb_nil_value();
int idx = shape_lookup(siv->shape, sym);
if (idx >= 0 && !mrb_undef_p(siv->values[idx]))
return siv->values[idx];
return mrb_nil_value();
}
static mrb_bool
shaped_iv_defined(struct RObject *obj, mrb_sym sym)
{
mrb_shaped_iv *siv = (mrb_shaped_iv*)obj->iv;
if (!siv) return FALSE;
int idx = shape_lookup(siv->shape, sym);
if (idx >= 0 && !mrb_undef_p(siv->values[idx]))
return TRUE;
return FALSE;
}
static void
shaped_iv_foreach(mrb_state *mrb, struct RObject *obj,
mrb_iv_foreach_func *func, void *p)
{
mrb_shaped_iv *siv = (mrb_shaped_iv*)obj->iv;
if (!siv) return;
mrb_iv_shape *shape = siv->shape;
if (shape->count == 0) return;
/* reconstruct keys from parent chain */
mrb_sym keys[MRB_SHAPE_MAX_IVS];
mrb_iv_shape *s = shape;
while (s->count > 0) {
keys[s->count - 1] = s->edge;
s = s->parent;
}
for (int i = 0; i < shape->count; i++) {
if (!mrb_undef_p(siv->values[i])) {
if ((*func)(mrb, keys[i], siv->values[i], p) != 0) return;
}
}
}
static size_t
shaped_iv_mark(mrb_state *mrb, struct RObject *obj)
{
mrb_shaped_iv *siv = (mrb_shaped_iv*)obj->iv;
if (!siv) return 0;
mrb_iv_shape *shape = siv->shape;
for (int i = 0; i < shape->count; i++) {
if (!mrb_undef_p(siv->values[i])) {
mrb_gc_mark_value(mrb, siv->values[i]);
}
}
return shape->count;
}
static void
shaped_iv_free(mrb_state *mrb, struct RObject *obj)
{
if (obj->iv) {
mrb_free(mrb, obj->iv);
}
}
static void
shaped_iv_copy(mrb_state *mrb, struct RObject *dst, struct RObject *src)
{
mrb_shaped_iv *ssiv = (mrb_shaped_iv*)src->iv;
if (!ssiv) {
dst->iv = NULL;
return;
}
mrb_iv_shape *shape = ssiv->shape;
mrb_shaped_iv *dsiv = shaped_iv_alloc(mrb, shape);
memcpy(dsiv->values, ssiv->values, sizeof(mrb_value) * shape->count);
dst->iv = (iv_tbl*)dsiv;
}
/* Public init/free for shape tree (called from state.c) */
void
mrb_init_shape(mrb_state *mrb)
{
mrb->root_shape = shape_root(mrb);
}
void
mrb_free_shape(mrb_state *mrb)
{
if (mrb->root_shape) {
shape_free_tree(mrb, mrb->root_shape);
mrb->root_shape = NULL;
}
}
static int
iv_mark_i(mrb_state *mrb, mrb_sym sym, mrb_value v, void *p)
{
@@ -270,6 +551,9 @@ mrb_gc_free_gv(mrb_state *mrb)
size_t
mrb_gc_mark_iv(mrb_state *mrb, struct RObject *obj)
{
if (MRB_OBJ_SHAPED_P(obj)) {
return shaped_iv_mark(mrb, obj);
}
mark_tbl(mrb, obj->iv);
return iv_size(mrb, obj->iv);
}
@@ -277,6 +561,10 @@ mrb_gc_mark_iv(mrb_state *mrb, struct RObject *obj)
void
mrb_gc_free_iv(mrb_state *mrb, struct RObject *obj)
{
if (MRB_OBJ_SHAPED_P(obj)) {
shaped_iv_free(mrb, obj);
return;
}
if (obj->iv) {
iv_free(mrb, obj->iv);
}
@@ -331,8 +619,11 @@ class_iv_ptr(struct RClass *c)
MRB_API mrb_value
mrb_obj_iv_get(mrb_state *mrb, struct RObject *obj, mrb_sym sym)
{
mrb_value v;
if (MRB_OBJ_SHAPED_P(obj)) {
return shaped_iv_get(obj, sym);
}
mrb_value v;
if (obj->iv && iv_get(mrb, obj->iv, sym, &v))
return v;
return mrb_nil_value();
@@ -402,6 +693,11 @@ mrb_obj_iv_set_force(mrb_state *mrb, struct RObject *obj, mrb_sym sym, mrb_value
if (namespace_p(obj->tt)) {
assign_class_name(mrb, obj, sym, v);
}
if (MRB_OBJ_SHAPED_P(obj)) {
shaped_iv_set(mrb, obj, sym, v);
mrb_field_write_barrier_value(mrb, (struct RBasic*)obj, v);
return;
}
if (!obj->iv) {
obj->iv = iv_new(mrb);
}
@@ -447,6 +743,10 @@ MRB_API void
mrb_iv_foreach(mrb_state *mrb, mrb_value obj, mrb_iv_foreach_func *func, void *p)
{
if (!obj_iv_p(obj)) return;
if (MRB_OBJ_SHAPED_P(mrb_obj_ptr(obj))) {
shaped_iv_foreach(mrb, mrb_obj_ptr(obj), func, p);
return;
}
iv_foreach(mrb, mrb_obj_ptr(obj)->iv, func, p);
}
@@ -492,9 +792,11 @@ mrb_iv_set(mrb_state *mrb, mrb_value obj, mrb_sym sym, mrb_value v)
MRB_API mrb_bool
mrb_obj_iv_defined(mrb_state *mrb, struct RObject *obj, mrb_sym sym)
{
iv_tbl *t;
if (MRB_OBJ_SHAPED_P(obj)) {
return shaped_iv_defined(obj, sym);
}
t = obj->iv;
iv_tbl *t = obj->iv;
if (t && iv_get(mrb, t, sym, NULL)) return TRUE;
return FALSE;
}
@@ -586,13 +888,55 @@ mrb_iv_copy(mrb_state *mrb, mrb_value dest, mrb_value src)
struct RObject *d = mrb_obj_ptr(dest);
struct RObject *s = mrb_obj_ptr(src);
if (d->iv) {
iv_free(mrb, d->iv);
d->iv = 0;
/* free dest's existing IVs */
if (MRB_OBJ_SHAPED_P(d)) {
shaped_iv_free(mrb, d);
d->iv = NULL;
}
if (s->iv) {
mrb_write_barrier(mrb, (struct RBasic*)d);
d->iv = iv_copy(mrb, s->iv);
else if (d->iv) {
iv_free(mrb, d->iv);
d->iv = NULL;
}
if (MRB_OBJ_SHAPED_P(s) && MRB_OBJ_SHAPED_P(d)) {
/* both shaped: share shape, memcpy values */
if (s->iv) {
mrb_write_barrier(mrb, (struct RBasic*)d);
shaped_iv_copy(mrb, d, s);
}
}
else if (MRB_OBJ_SHAPED_P(s)) {
/* src shaped, dest unshaped: convert src to iv_tbl copy */
mrb_shaped_iv *ssiv = (mrb_shaped_iv*)s->iv;
if (ssiv) {
mrb_iv_shape *shape = ssiv->shape;
if (shape->count > 0) {
mrb_sym keys[MRB_SHAPE_MAX_IVS];
mrb_iv_shape *sh = shape;
while (sh->count > 0) {
keys[sh->count - 1] = sh->edge;
sh = sh->parent;
}
iv_tbl *t = iv_new(mrb);
for (int i = 0; i < shape->count; i++) {
if (!mrb_undef_p(ssiv->values[i])) {
iv_put(mrb, t, keys[i], ssiv->values[i]);
}
}
mrb_write_barrier(mrb, (struct RBasic*)d);
d->iv = t;
}
}
}
else {
/* both unshaped or dest shaped but src unshaped */
if (MRB_OBJ_SHAPED_P(d)) {
d->flags &= ~MRB_FL_OBJ_SHAPED;
}
if (s->iv) {
mrb_write_barrier(mrb, (struct RBasic*)d);
d->iv = iv_copy(mrb, s->iv);
}
}
}
@@ -614,10 +958,25 @@ mrb_iv_remove(mrb_state *mrb, mrb_value obj, mrb_sym sym)
{
if (obj_iv_p(obj)) {
struct RObject *o = mrb_obj_ptr(obj);
mrb_check_frozen(mrb, o);
if (MRB_OBJ_SHAPED_P(o)) {
mrb_shaped_iv *siv = (mrb_shaped_iv*)o->iv;
if (siv) {
int idx = shape_lookup(siv->shape, sym);
if (idx >= 0 && !mrb_undef_p(siv->values[idx])) {
mrb_value val = siv->values[idx];
/* de-shape, then remove the key */
shaped_to_iv_tbl(mrb, o);
iv_del(mrb, o->iv, sym, NULL);
return val;
}
}
return mrb_undef_value();
}
iv_tbl *t = o->iv;
mrb_value val;
mrb_check_frozen(mrb, o);
if (iv_del(mrb, t, sym, &val)) {
return val;
}
@@ -661,7 +1020,13 @@ mrb_obj_instance_variables(mrb_state *mrb, mrb_value self)
mrb_value ary = mrb_ary_new(mrb);
if (obj_iv_p(self)) {
iv_foreach(mrb, mrb_obj_ptr(self)->iv, iv_i, &ary);
struct RObject *obj = mrb_obj_ptr(self);
if (MRB_OBJ_SHAPED_P(obj)) {
shaped_iv_foreach(mrb, obj, iv_i, &ary);
}
else {
iv_foreach(mrb, obj->iv, iv_i, &ary);
}
}
return ary;
}
@@ -1458,7 +1823,14 @@ mrb_class_find_path(mrb_state *mrb, struct RClass *c)
size_t
mrb_obj_iv_tbl_memsize(mrb_value obj)
{
iv_tbl *t = mrb_obj_ptr(obj)->iv;
struct RObject *o = mrb_obj_ptr(obj);
if (MRB_OBJ_SHAPED_P(o)) {
mrb_shaped_iv *siv = (mrb_shaped_iv*)o->iv;
if (!siv) return 0;
return offsetof(mrb_shaped_iv, values) +
sizeof(mrb_value) * siv->shape->count;
}
iv_tbl *t = o->iv;
if (t == NULL) return 0;
return sizeof(iv_tbl) + t->alloc*(sizeof(mrb_value)+sizeof(mrb_sym));
}