Read the object_id of a shareable T_DATA without the VM lock

object_id took the VM lock for every shareable object in multi-Ractor mode, even
when the object already had an id.

Kernel#hash reaches object_id through rb_obj_hash, so a shareable T_DATA such as
an Encoding, a Ractor or a Ractor::Port used as a Hash key serialised every
lookup across Ractors.

In this commit we make generic_field_set copy the fields of a shareable object
in multi-Ractor mode, and publish the new fields_obj of a T_DATA with an atomic
write.

```ruby
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
ractors = 4.times.map do
  Ractor.new do
    h = { Encoding::UTF_8 => 1 }
    1_000_000.times { h[Encoding::UTF_8] }
  end
end
ractors.each(&:join)
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t
puts "#{elapsed.round(2)}s"
```

I ran this 3 times before this patch and after and saw

    before: 0.58s 0.61s 0.64s
    after:  0.05s 0.05s 0.05s
This commit is contained in:
Matt Valentine-House 2026-11-05 22:05:42 +00:00 committed by eightbitraptor
parent 1f44a8642a
commit 2a8f329f59
Notes: git 2026-11-06 20:27:13 +00:00
3 changed files with 40 additions and 13 deletions

33
gc.c
View File

@ -2402,16 +2402,16 @@ class_object_id(VALUE klass)
return id;
}
static inline shape_id_t
object_id_field_shape(shape_id_t shape_id)
{
return rb_shape_complex_p(shape_id) ? ROOT_COMPLEX_WITH_OBJ_ID : rb_shape_object_id(shape_id);
}
static inline VALUE
object_id_get(VALUE obj, shape_id_t shape_id)
{
VALUE id;
if (rb_shape_complex_p(shape_id)) {
id = rb_obj_field_get(obj, ROOT_COMPLEX_WITH_OBJ_ID);
}
else {
id = rb_obj_field_get(obj, rb_shape_object_id(shape_id));
}
VALUE id = rb_obj_field_get(obj, object_id_field_shape(shape_id));
#if RUBY_DEBUG
if (!(FIXNUM_P(id) || RB_TYPE_P(id, T_BIGNUM))) {
@ -2443,6 +2443,20 @@ object_id0(VALUE obj)
return id;
}
// generic_field_set never mutates the published fields_obj of a shareable object,
// and the fields_obj carries its owner's shape, so both are read from the one snapshot.
static VALUE
shareable_data_object_id(VALUE obj)
{
VALUE fields_obj = RUBY_ATOMIC_VALUE_LOAD(RTYPEDDATA(obj)->fields_obj);
if (!fields_obj) return Qfalse;
shape_id_t shape_id = RBASIC_SHAPE_ID(fields_obj);
if (!rb_shape_has_object_id(shape_id)) return Qfalse;
return rb_imemo_fields_field_get(fields_obj, object_id_field_shape(shape_id));
}
static VALUE
object_id(VALUE obj)
{
@ -2461,6 +2475,11 @@ object_id(VALUE obj)
}
if (UNLIKELY(rb_gc_multi_ractor_p() && rb_ractor_shareable_p(obj))) {
if (BUILTIN_TYPE(obj) == T_DATA) {
VALUE existing_id = shareable_data_object_id(obj);
if (existing_id) return existing_id;
}
unsigned int lock_lev = RB_GC_VM_LOCK();
VALUE id = object_id0(obj);
RB_GC_VM_UNLOCK(lock_lev);

View File

@ -49,6 +49,7 @@ VALUE rb_mod_set_temporary_name(VALUE, VALUE);
void rb_obj_replace_fields(VALUE obj, VALUE fields_obj);
VALUE rb_obj_complex_fields_build(VALUE obj);
VALUE rb_obj_field_get(VALUE obj, shape_id_t target_shape_id);
VALUE rb_imemo_fields_field_get(VALUE fields_obj, shape_id_t target_shape_id);
void rb_ivar_set_internal(VALUE obj, ID id, VALUE val);
void rb_check_ivar_modifiable(VALUE obj);
void rb_ivar_foreach_buffered(VALUE obj, int (*func)(ID name, VALUE val, st_data_t arg), st_data_t arg);

View File

@ -1511,7 +1511,7 @@ rb_obj_set_fields(VALUE obj, VALUE fields_obj, ID field_name, VALUE original_fie
ROBJECT_SET_EXTENDED(obj, fields_obj);
break;
case T_DATA:
RB_OBJ_WRITE(obj, &RTYPEDDATA(obj)->fields_obj, fields_obj);
RB_OBJ_ATOMIC_WRITE(obj, &RTYPEDDATA(obj)->fields_obj, fields_obj);
break;
case T_STRUCT:
RSTRUCT_SET_FIELDS_OBJ(obj, fields_obj);
@ -1577,13 +1577,10 @@ rb_obj_replace_fields(VALUE obj, VALUE fields_obj)
}
VALUE
rb_obj_field_get(VALUE obj, shape_id_t target_shape_id)
rb_imemo_fields_field_get(VALUE fields_obj, shape_id_t target_shape_id)
{
RUBY_ASSERT(!SPECIAL_CONST_P(obj));
RUBY_ASSERT(RSHAPE_TYPE_P(target_shape_id, SHAPE_IVAR) || RSHAPE_TYPE_P(target_shape_id, SHAPE_OBJ_ID));
VALUE fields_obj = rb_obj_fields(obj, RSHAPE_EDGE_NAME(target_shape_id));
if (UNLIKELY(rb_shape_complex_p(target_shape_id))) {
st_table *fields_hash = rb_imemo_fields_complex_tbl(fields_obj);
VALUE value = Qundef;
@ -1596,6 +1593,14 @@ rb_obj_field_get(VALUE obj, shape_id_t target_shape_id)
return rb_imemo_fields_ptr(fields_obj)[index];
}
VALUE
rb_obj_field_get(VALUE obj, shape_id_t target_shape_id)
{
RUBY_ASSERT(!SPECIAL_CONST_P(obj));
return rb_imemo_fields_field_get(rb_obj_fields(obj, RSHAPE_EDGE_NAME(target_shape_id)), target_shape_id);
}
VALUE
rb_ivar_lookup(VALUE obj, ID id, VALUE undef)
{
@ -1964,7 +1969,9 @@ generic_field_set(VALUE obj, shape_id_t target_shape_id, ID field_name, VALUE va
}
const VALUE original_fields_obj = rb_obj_fields(obj, field_name);
VALUE fields_obj = imemo_fields_set(obj, original_fields_obj, target_shape_id, field_name, val, false);
// Readers of a shareable object's fields take no lock, so never mutate a published fields_obj.
bool concurrent = rb_multi_ractor_p() && RB_OBJ_SHAREABLE_P(obj);
VALUE fields_obj = imemo_fields_set(obj, original_fields_obj, target_shape_id, field_name, val, concurrent);
rb_obj_set_fields(obj, fields_obj, field_name, original_fields_obj);
return rb_shape_complex_p(target_shape_id) ? ATTR_INDEX_NOT_SET : RSHAPE_INDEX(target_shape_id);