shape.c: Implement a lock-free version of get_next_shape_internal

Whenever we run into an inline cache miss when we try to set
an ivar, we may need to take the global lock, just to be able to
lookup inside `shape->edges`.

To solve that, when we're in multi-ractor mode, we can treat
the `shape->edges` as immutable. When we need to add a new
edge, we first copy the table, and then replace it with
CAS.

This increases memory allocations, however we expect that
creating new transitions becomes increasingly rare over time.

```ruby
class A
  def initialize(bool)
    @a = 1
    if bool
      @b = 2
    else
      @c = 3
    end
  end

  def test
    @d = 4
  end
end

def bench(iterations)
  i = iterations
  while i > 0
    A.new(true).test
    A.new(false).test
    i -= 1
  end
end

if ARGV.first == "ractor"
  ractors = 8.times.map do
    Ractor.new do
      bench(20_000_000 / 8)
    end
  end
  ractors.each(&:take)
else
  bench(20_000_000)
end
```

The above benchmark takes 27 seconds in Ractor mode on Ruby 3.4,
and only 1.7s with this branch.

Co-Authored-By: Étienne Barrié <etienne.barrie@gmail.com>
This commit is contained in:
Jean Boussier 2025-05-19 12:38:49 +02:00
parent cbd49ecbbe
commit e9fd44dd72
Notes: git 2025-06-02 15:50:08 +00:00
8 changed files with 307 additions and 107 deletions

View file

@ -80,9 +80,10 @@ round_capa(int capa)
return (capa + 1) << 2;
}
static struct rb_id_table *
rb_id_table_init(struct rb_id_table *tbl, int capa)
struct rb_id_table *
rb_id_table_init(struct rb_id_table *tbl, size_t s_capa)
{
int capa = (int)s_capa;
MEMZERO(tbl, struct rb_id_table, 1);
if (capa > 0) {
capa = round_capa(capa);
@ -96,7 +97,13 @@ struct rb_id_table *
rb_id_table_create(size_t capa)
{
struct rb_id_table *tbl = ALLOC(struct rb_id_table);
return rb_id_table_init(tbl, (int)capa);
return rb_id_table_init(tbl, capa);
}
void
rb_id_table_free_items(struct rb_id_table *tbl)
{
xfree(tbl->items);
}
void
@ -324,3 +331,94 @@ rb_id_table_foreach_values_with_replace(struct rb_id_table *tbl, rb_id_table_for
}
}
static void
managed_id_table_free(void *data)
{
struct rb_id_table *tbl = (struct rb_id_table *)data;
rb_id_table_free_items(tbl);
}
static size_t
managed_id_table_memsize(const void *data)
{
const struct rb_id_table *tbl = (const struct rb_id_table *)data;
return rb_id_table_memsize(tbl) - sizeof(struct rb_id_table);
}
static const rb_data_type_t managed_id_table_type = {
.wrap_struct_name = "VM/managed_id_table",
.function = {
.dmark = NULL, // Nothing to mark
.dfree = (RUBY_DATA_FUNC)managed_id_table_free,
.dsize = managed_id_table_memsize,
},
.flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE,
};
static inline struct rb_id_table *
managed_id_table_ptr(VALUE obj)
{
return RTYPEDDATA_GET_DATA(obj);
}
VALUE
rb_managed_id_table_new(size_t capa)
{
struct rb_id_table *tbl;
VALUE obj = TypedData_Make_Struct(0, struct rb_id_table, &managed_id_table_type, tbl);
rb_id_table_init(tbl, capa);
return obj;
}
static enum rb_id_table_iterator_result
managed_id_table_dup_i(ID id, VALUE val, void *data)
{
struct rb_id_table *new_tbl = (struct rb_id_table *)data;
rb_id_table_insert(new_tbl, id, val);
return ID_TABLE_CONTINUE;
}
VALUE
rb_managed_id_table_dup(VALUE old_table)
{
RUBY_ASSERT(rb_typeddata_inherited_p(RTYPEDDATA_TYPE(old_table), &managed_id_table_type));
struct rb_id_table *new_tbl;
VALUE obj = TypedData_Make_Struct(0, struct rb_id_table, &managed_id_table_type, new_tbl);
struct rb_id_table *old_tbl = RTYPEDDATA_GET_DATA(old_table);
rb_id_table_init(new_tbl, old_tbl->num + 1);
rb_id_table_foreach(old_tbl, managed_id_table_dup_i, new_tbl);
return obj;
}
int
rb_managed_id_table_lookup(VALUE table, ID id, VALUE *valp)
{
RUBY_ASSERT(rb_typeddata_inherited_p(RTYPEDDATA_TYPE(table), &managed_id_table_type));
return rb_id_table_lookup(RTYPEDDATA_GET_DATA(table), id, valp);
}
int
rb_managed_id_table_insert(VALUE table, ID id, VALUE val)
{
RUBY_ASSERT(rb_typeddata_inherited_p(RTYPEDDATA_TYPE(table), &managed_id_table_type));
return rb_id_table_insert(RTYPEDDATA_GET_DATA(table), id, val);
}
size_t
rb_managed_id_table_size(VALUE table)
{
RUBY_ASSERT(rb_typeddata_inherited_p(RTYPEDDATA_TYPE(table), &managed_id_table_type));
return rb_id_table_size(RTYPEDDATA_GET_DATA(table));
}
void
rb_managed_id_table_foreach(VALUE table, rb_id_table_foreach_func_t *func, void *data)
{
RUBY_ASSERT(rb_typeddata_inherited_p(RTYPEDDATA_TYPE(table), &managed_id_table_type));
rb_id_table_foreach(RTYPEDDATA_GET_DATA(table), func, data);
}