Skip to content

Commit 97fb07d

Browse files
byrootXrXr
andcommittedNov 18, 2023
Pin object ivars while they are being copied in a hash
When transitioning an object to TOO_COMPLEX we copy all its ivar in a table, but if GC (and compaction) trigger in the middle of the transition, the old `iv_ptr` might still be the canonical ivar list and will be updated by the GC, but the reference we copied in the table will be outdated. So we must pin these reference until they are all copied and the object `iv_ptr` is pointing to the table. To do this we allocate a temporary TOO_COMPLEX object which we use as the host for the copy. TOO_COMPLEX objects are marked with `mark_tbl` which does pin references. Co-Authored-By: Alan Wu <XrXr@users.noreply.github.com>
1 parent f479e62 commit 97fb07d

File tree

4 files changed

+146
-10
lines changed

4 files changed

+146
-10
lines changed
 

‎internal/variable.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ VALUE rb_mod_set_temporary_name(VALUE, VALUE);
4747

4848
struct gen_ivtbl;
4949
int rb_gen_ivtbl_get(VALUE obj, ID id, struct gen_ivtbl **ivtbl);
50-
void rb_obj_copy_ivs_to_hash_table(VALUE obj, st_table *table);
50+
VALUE rb_obj_copy_ivs_to_hash_table(VALUE obj, st_table *table);
51+
void rb_obj_copy_ivs_to_hash_table_complete(VALUE ivar_pinner);
5152
void rb_obj_convert_to_too_complex(VALUE obj, st_table *table);
5253
void rb_evict_ivars_to_hash(VALUE obj);
5354

‎object.c

+2-1
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,9 @@ rb_obj_copy_ivar(VALUE dest, VALUE obj)
326326
shape_to_set_on_dest = rb_shape_rebuild_shape(initial_shape, src_shape);
327327
if (UNLIKELY(rb_shape_id(shape_to_set_on_dest) == OBJ_TOO_COMPLEX_SHAPE_ID)) {
328328
st_table * table = rb_st_init_numtable_with_size(src_num_ivs);
329-
rb_obj_copy_ivs_to_hash_table(obj, table);
329+
VALUE ivar_pinner = rb_obj_copy_ivs_to_hash_table(obj, table);
330330
rb_obj_convert_to_too_complex(dest, table);
331+
rb_obj_copy_ivs_to_hash_table_complete(ivar_pinner);
331332

332333
return;
333334
}

‎test/ruby/test_shapes.rb

+99-2
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,9 @@ def test_run_out_of_shape_for_class_ivar
256256
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
257257
begin;
258258
i = 0
259+
o = Object.new
259260
while RubyVM::Shape.shapes_available > 0
260-
c = Class.new
261-
c.instance_variable_set(:"@i#{i}", 1)
261+
o.instance_variable_set(:"@i#{i}", 1)
262262
i += 1
263263
end
264264
@@ -275,6 +275,103 @@ def test_run_out_of_shape_for_class_ivar
275275
end;
276276
end
277277

278+
def test_evacuate_class_ivar_and_compaction
279+
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
280+
281+
begin;
282+
count = 20
283+
284+
c = Class.new
285+
count.times do |ivar|
286+
c.instance_variable_set("@i#{ivar}", "ivar-#{ivar}")
287+
end
288+
289+
i = 0
290+
o = Object.new
291+
while RubyVM::Shape.shapes_available > 0
292+
o.instance_variable_set("@i#{i}", 1)
293+
i += 1
294+
end
295+
296+
GC.auto_compact = true
297+
GC.stress = true
298+
# Cause evacuation
299+
c.instance_variable_set(:@a, o = Object.new)
300+
assert_equal(o, c.instance_variable_get(:@a))
301+
GC.stress = false
302+
303+
count.times do |ivar|
304+
assert_equal "ivar-#{ivar}", c.instance_variable_get("@i#{ivar}")
305+
end
306+
end;
307+
end
308+
309+
def test_evacuate_generic_ivar_and_compaction
310+
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
311+
312+
begin;
313+
count = 20
314+
315+
c = Hash.new
316+
count.times do |ivar|
317+
c.instance_variable_set("@i#{ivar}", "ivar-#{ivar}")
318+
end
319+
320+
i = 0
321+
o = Object.new
322+
while RubyVM::Shape.shapes_available > 0
323+
o.instance_variable_set("@i#{i}", 1)
324+
i += 1
325+
end
326+
327+
GC.auto_compact = true
328+
GC.stress = true
329+
330+
# Cause evacuation
331+
c.instance_variable_set(:@a, o = Object.new)
332+
assert_equal(o, c.instance_variable_get(:@a))
333+
334+
GC.stress = false
335+
336+
count.times do |ivar|
337+
assert_equal "ivar-#{ivar}", c.instance_variable_get("@i#{ivar}")
338+
end
339+
end;
340+
end
341+
342+
def test_evacuate_object_ivar_and_compaction
343+
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
344+
345+
begin;
346+
count = 20
347+
348+
c = Object.new
349+
count.times do |ivar|
350+
c.instance_variable_set("@i#{ivar}", "ivar-#{ivar}")
351+
end
352+
353+
i = 0
354+
o = Object.new
355+
while RubyVM::Shape.shapes_available > 0
356+
o.instance_variable_set("@i#{i}", 1)
357+
i += 1
358+
end
359+
360+
GC.auto_compact = true
361+
GC.stress = true
362+
363+
# Cause evacuation
364+
c.instance_variable_set(:@a, o = Object.new)
365+
assert_equal(o, c.instance_variable_get(:@a))
366+
367+
GC.stress = false
368+
369+
count.times do |ivar|
370+
assert_equal "ivar-#{ivar}", c.instance_variable_get("@i#{ivar}")
371+
end
372+
end;
373+
end
374+
278375
def test_run_out_of_shape_for_module_ivar
279376
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
280377
begin;

‎variable.c

+43-6
Original file line numberDiff line numberDiff line change
@@ -1370,13 +1370,22 @@ rb_attr_delete(VALUE obj, ID id)
13701370
return rb_ivar_delete(obj, id, Qnil);
13711371
}
13721372

1373+
static int
1374+
rb_obj_write_barrier_ivars_i(ID key, VALUE val, st_data_t arg)
1375+
{
1376+
RB_OBJ_WRITTEN((VALUE)arg, Qundef, val);
1377+
return ST_CONTINUE;
1378+
}
1379+
13731380
void
13741381
rb_obj_convert_to_too_complex(VALUE obj, st_table *table)
13751382
{
13761383
RUBY_ASSERT(!rb_shape_obj_too_complex(obj));
13771384

13781385
VALUE *old_ivptr = NULL;
13791386

1387+
rb_ivar_foreach(obj, rb_obj_write_barrier_ivars_i, (st_data_t)obj);
1388+
13801389
switch (BUILTIN_TYPE(obj)) {
13811390
case T_OBJECT:
13821391
if (!(RBASIC(obj)->flags & ROBJECT_EMBED)) {
@@ -1422,8 +1431,9 @@ rb_evict_ivars_to_hash(VALUE obj)
14221431
st_table *table = st_init_numtable_with_size(rb_ivar_count(obj));
14231432

14241433
// Evacuate all previous values from shape into id_table
1425-
rb_obj_copy_ivs_to_hash_table(obj, table);
1434+
VALUE ivar_pinner = rb_obj_copy_ivs_to_hash_table(obj, table);
14261435
rb_obj_convert_to_too_complex(obj, table);
1436+
rb_obj_copy_ivs_to_hash_table_complete(ivar_pinner);
14271437

14281438
RUBY_ASSERT(rb_shape_obj_too_complex(obj));
14291439
}
@@ -1640,17 +1650,44 @@ rb_ensure_iv_list_size(VALUE obj, uint32_t current_capacity, uint32_t new_capaci
16401650
}
16411651
}
16421652

1643-
int
1644-
rb_obj_copy_ivs_to_hash_table_i(ID key, VALUE val, st_data_t arg)
1653+
struct rb_evacuate_arg {
1654+
st_table *table;
1655+
VALUE host;
1656+
};
1657+
1658+
static int
1659+
copy_ivs_to_hash_table_i(ID key, VALUE val, st_data_t arg)
16451660
{
1646-
st_insert((st_table *)arg, (st_data_t)key, (st_data_t)val);
1661+
struct rb_evacuate_arg *evac_arg = (struct rb_evacuate_arg *)arg;
1662+
st_insert(evac_arg->table, (st_data_t)key, (st_data_t)val);
1663+
RB_OBJ_WRITTEN(evac_arg->host, Qundef, val);
16471664
return ST_CONTINUE;
16481665
}
16491666

1650-
void
1667+
VALUE
16511668
rb_obj_copy_ivs_to_hash_table(VALUE obj, st_table *table)
16521669
{
1653-
rb_ivar_foreach(obj, rb_obj_copy_ivs_to_hash_table_i, (st_data_t)table);
1670+
// There can be compaction runs between each ivar we copy out of obj, so we
1671+
// need an object to mark each ivar to make sure every reference is valid
1672+
// by the time we're done. Use a special T_OBJECT for marking.
1673+
VALUE ivar_pinner = rb_wb_protected_newobj_of(GET_EC(), rb_cBasicObject, T_OBJECT | ROBJECT_EMBED, RVALUE_SIZE);
1674+
rb_shape_set_shape_id(ivar_pinner, OBJ_TOO_COMPLEX_SHAPE_ID);
1675+
ROBJECT_SET_IV_HASH(ivar_pinner, table);
1676+
1677+
// Evacuate all previous values from shape into id_table
1678+
struct rb_evacuate_arg evac_arg = { .table = table, .host = ivar_pinner };
1679+
rb_ivar_foreach(obj, copy_ivs_to_hash_table_i, (st_data_t)&evac_arg);
1680+
1681+
return ivar_pinner;
1682+
}
1683+
1684+
void
1685+
rb_obj_copy_ivs_to_hash_table_complete(VALUE ivar_pinner)
1686+
{
1687+
// done with pinning, now set pinner to 0 ivars
1688+
ROBJECT_SET_IV_HASH(ivar_pinner, NULL);
1689+
rb_shape_set_shape(ivar_pinner, rb_shape_get_root_shape());
1690+
RB_GC_GUARD(ivar_pinner);
16541691
}
16551692

16561693
static VALUE *

0 commit comments

Comments
 (0)
Failed to load comments.