Skip to content

Commit c39e128

Browse files
committed
Refactor vm_lookup_cc to allow lock-free lookups in RClass.cc_tbl
In multi-ractor mode, the `cc_tbl` mutations use the RCU pattern, which allow lock-less reads. Based on the assumption that invalidations and misses should be increasingly rare as the process ages, locking on modification isn't a big concern.
1 parent 994f496 commit c39e128

6 files changed

Lines changed: 212 additions & 101 deletions

File tree

bootstraptest/test_ractor.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2315,3 +2315,33 @@ def Warning.warn(msg)
23152315
raise unless $msg.all?{/Ractor#take/ =~ it}
23162316
$msg.size
23172317
}
2318+
2319+
# Cause lots of inline CC misses.
2320+
assert_equal 'ok', <<~'RUBY'
2321+
class A; def test; 1 + 1; end; end
2322+
class B; def test; 1 + 1; end; end
2323+
class C; def test; 1 + 1; end; end
2324+
class D; def test; 1 + 1; end; end
2325+
class E; def test; 1 + 1; end; end
2326+
class F; def test; 1 + 1; end; end
2327+
class G; def test; 1 + 1; end; end
2328+
2329+
objs = [A.new, B.new, C.new, D.new, E.new, F.new, G.new].freeze
2330+
2331+
def call_test(obj)
2332+
obj.test
2333+
end
2334+
2335+
ractors = 7.times.map do
2336+
Ractor.new(objs) do |objs|
2337+
objs = objs.shuffle
2338+
100_000.times do
2339+
objs.each do |o|
2340+
call_test(o)
2341+
end
2342+
end
2343+
end
2344+
end
2345+
ractors.each(&:join)
2346+
:ok
2347+
RUBY

id_table.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ VALUE
395395
rb_managed_id_table_dup(VALUE old_table)
396396
{
397397
struct rb_id_table *new_tbl;
398-
VALUE obj = TypedData_Make_Struct(0, struct rb_id_table, &rb_managed_id_table_type, new_tbl);
398+
VALUE obj = TypedData_Make_Struct(0, struct rb_id_table, RTYPEDDATA_TYPE(old_table), new_tbl);
399399
struct rb_id_table *old_tbl = managed_id_table_ptr(old_table);
400400
rb_id_table_init(new_tbl, old_tbl->num + 1);
401401
rb_id_table_foreach(old_tbl, managed_id_table_dup_i, new_tbl);

imemo.c

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -516,41 +516,6 @@ rb_free_const_table(struct rb_id_table *tbl)
516516
rb_id_table_free(tbl);
517517
}
518518

519-
// alive: if false, target pointers can be freed already.
520-
static void
521-
vm_ccs_free(struct rb_class_cc_entries *ccs, int alive, VALUE klass)
522-
{
523-
if (ccs->entries) {
524-
for (int i=0; i<ccs->len; i++) {
525-
const struct rb_callcache *cc = ccs->entries[i].cc;
526-
if (!alive) {
527-
// ccs can be free'ed.
528-
if (rb_gc_pointer_to_heap_p((VALUE)cc) &&
529-
!rb_objspace_garbage_object_p((VALUE)cc) &&
530-
IMEMO_TYPE_P(cc, imemo_callcache) &&
531-
cc->klass == klass) {
532-
// OK. maybe target cc.
533-
}
534-
else {
535-
continue;
536-
}
537-
}
538-
539-
VM_ASSERT(!vm_cc_super_p(cc) && !vm_cc_refinement_p(cc));
540-
vm_cc_invalidate(cc);
541-
}
542-
ruby_xfree(ccs->entries);
543-
}
544-
ruby_xfree(ccs);
545-
}
546-
547-
void
548-
rb_vm_ccs_free(struct rb_class_cc_entries *ccs)
549-
{
550-
RB_DEBUG_COUNTER_INC(ccs_free);
551-
vm_ccs_free(ccs, true, Qundef);
552-
}
553-
554519
static inline void
555520
imemo_fields_free(struct rb_fields *fields)
556521
{

vm_callinfo.h

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@ cc_check_class(VALUE klass)
330330
}
331331

332332
VALUE rb_vm_cc_table_create(size_t capa);
333+
VALUE rb_vm_cc_table_dup(VALUE old_table);
334+
void rb_vm_cc_table_delete(VALUE table, ID mid);
333335

334336
static inline const struct rb_callcache *
335337
vm_cc_new(VALUE klass,
@@ -600,11 +602,14 @@ vm_ccs_p(const struct rb_class_cc_entries *ccs)
600602
static inline bool
601603
vm_cc_check_cme(const struct rb_callcache *cc, const rb_callable_method_entry_t *cme)
602604
{
603-
if (vm_cc_cme(cc) == cme ||
604-
(cme->def->iseq_overload && vm_cc_cme(cc) == rb_vm_lookup_overloaded_cme(cme))) {
605+
bool valid;
606+
RB_VM_LOCKING() {
607+
valid = vm_cc_cme(cc) == cme ||
608+
(cme->def->iseq_overload && vm_cc_cme(cc) == rb_vm_lookup_overloaded_cme(cme));
609+
}
610+
if (valid) {
605611
return true;
606612
}
607-
else {
608613
#if 1
609614
// debug print
610615

@@ -616,13 +621,9 @@ vm_cc_check_cme(const struct rb_callcache *cc, const rb_callable_method_entry_t
616621
rp(vm_cc_cme(cc));
617622
rp(rb_vm_lookup_overloaded_cme(cme));
618623
#endif
619-
return false;
620-
}
624+
return false;
621625
}
622626

623627
#endif
624628

625-
// gc.c
626-
void rb_vm_ccs_free(struct rb_class_cc_entries *ccs);
627-
628629
#endif /* RUBY_VM_CALLINFO_H */

vm_insnhelper.c

Lines changed: 127 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2063,58 +2063,49 @@ vm_ccs_verify(struct rb_class_cc_entries *ccs, ID mid, VALUE klass)
20632063

20642064
const rb_callable_method_entry_t *rb_check_overloaded_cme(const rb_callable_method_entry_t *cme, const struct rb_callinfo * const ci);
20652065

2066-
static const struct rb_callcache *
2067-
vm_search_cc(const VALUE klass, const struct rb_callinfo * const ci)
2066+
static void
2067+
vm_evict_cc(VALUE klass, VALUE cc_tbl, ID mid)
20682068
{
2069-
const ID mid = vm_ci_mid(ci);
2070-
VALUE cc_tbl = RCLASS_WRITABLE_CC_TBL(klass);
2071-
struct rb_class_cc_entries *ccs = NULL;
2072-
VALUE ccs_data;
2069+
ASSERT_vm_locking();
20732070

2074-
if (cc_tbl) {
2075-
// CCS data is keyed on method id, so we don't need the method id
2076-
// for doing comparisons in the `for` loop below.
2077-
if (rb_managed_id_table_lookup(cc_tbl, mid, &ccs_data)) {
2078-
ccs = (struct rb_class_cc_entries *)ccs_data;
2079-
const int ccs_len = ccs->len;
2080-
2081-
if (UNLIKELY(METHOD_ENTRY_INVALIDATED(ccs->cme))) {
2082-
rb_managed_id_table_delete(cc_tbl, mid);
2083-
rb_vm_ccs_free(ccs);
2084-
ccs = NULL;
2085-
}
2086-
else {
2087-
VM_ASSERT(vm_ccs_verify(ccs, mid, klass));
2071+
if (rb_multi_ractor_p()) {
2072+
if (RCLASS_WRITABLE_CC_TBL(klass) != cc_tbl) {
2073+
// Another ractor updated the CC table while we were waiting on the VM lock.
2074+
// We have to retry.
2075+
return;
2076+
}
20882077

2089-
// We already know the method id is correct because we had
2090-
// to look up the ccs_data by method id. All we need to
2091-
// compare is argc and flag
2092-
unsigned int argc = vm_ci_argc(ci);
2093-
unsigned int flag = vm_ci_flag(ci);
2078+
struct rb_class_cc_entries *ccs = NULL;
2079+
rb_managed_id_table_lookup(cc_tbl, mid, (VALUE *)&ccs);
20942080

2095-
for (int i=0; i<ccs_len; i++) {
2096-
unsigned int ccs_ci_argc = ccs->entries[i].argc;
2097-
unsigned int ccs_ci_flag = ccs->entries[i].flag;
2098-
const struct rb_callcache *ccs_cc = ccs->entries[i].cc;
2081+
if (!ccs || !METHOD_ENTRY_INVALIDATED(ccs->cme)) {
2082+
// Another ractor replaced that entry while we were waiting on the VM lock.
2083+
return;
2084+
}
20992085

2100-
VM_ASSERT(IMEMO_TYPE_P(ccs_cc, imemo_callcache));
2086+
VALUE new_table = rb_vm_cc_table_dup(cc_tbl);
2087+
rb_vm_cc_table_delete(new_table, mid);
2088+
RB_OBJ_ATOMIC_WRITE(klass, &RCLASS_WRITABLE_CC_TBL(klass), new_table);
2089+
}
2090+
else {
2091+
rb_vm_cc_table_delete(cc_tbl, mid);
2092+
}
2093+
}
21012094

2102-
if (ccs_ci_argc == argc && ccs_ci_flag == flag) {
2103-
RB_DEBUG_COUNTER_INC(cc_found_in_ccs);
2095+
static const struct rb_callcache *
2096+
vm_populate_cc(VALUE klass, const struct rb_callinfo * const ci, ID mid)
2097+
{
2098+
ASSERT_vm_locking();
21042099

2105-
VM_ASSERT(vm_cc_cme(ccs_cc)->called_id == mid);
2106-
VM_ASSERT(ccs_cc->klass == klass);
2107-
VM_ASSERT(!METHOD_ENTRY_INVALIDATED(vm_cc_cme(ccs_cc)));
2100+
VALUE cc_tbl = RCLASS_WRITABLE_CC_TBL(klass);
2101+
const VALUE original_cc_table = cc_tbl;
2102+
struct rb_class_cc_entries *ccs = NULL;
21082103

2109-
return ccs_cc;
2110-
}
2111-
}
2112-
}
2113-
}
2104+
if (!cc_tbl) {
2105+
cc_tbl = rb_vm_cc_table_create(1);
21142106
}
2115-
else {
2116-
cc_tbl = rb_vm_cc_table_create(2);
2117-
RCLASS_WRITE_CC_TBL(klass, cc_tbl);
2107+
else if (rb_multi_ractor_p()) {
2108+
cc_tbl = rb_vm_cc_table_dup(cc_tbl);
21182109
}
21192110

21202111
RB_DEBUG_COUNTER_INC(cc_not_found_in_ccs);
@@ -2146,11 +2137,7 @@ vm_search_cc(const VALUE klass, const struct rb_callinfo * const ci)
21462137
if (ccs == NULL) {
21472138
VM_ASSERT(cc_tbl);
21482139

2149-
if (LIKELY(rb_managed_id_table_lookup(cc_tbl, mid, &ccs_data))) {
2150-
// rb_callable_method_entry() prepares ccs.
2151-
ccs = (struct rb_class_cc_entries *)ccs_data;
2152-
}
2153-
else {
2140+
if (!LIKELY(rb_managed_id_table_lookup(cc_tbl, mid, (VALUE *)&ccs))) {
21542141
// TODO: required?
21552142
ccs = vm_ccs_create(klass, cc_tbl, mid, cme);
21562143
}
@@ -2165,6 +2152,91 @@ vm_search_cc(const VALUE klass, const struct rb_callinfo * const ci)
21652152
VM_ASSERT(cme->called_id == mid);
21662153
VM_ASSERT(vm_cc_cme(cc)->called_id == mid);
21672154

2155+
if (original_cc_table != cc_tbl) {
2156+
RB_OBJ_ATOMIC_WRITE(klass, &RCLASS_WRITABLE_CC_TBL(klass), cc_tbl);
2157+
}
2158+
2159+
return cc;
2160+
}
2161+
2162+
static const struct rb_callcache *
2163+
vm_lookup_cc(const VALUE klass, const struct rb_callinfo * const ci, ID mid)
2164+
{
2165+
VALUE cc_tbl;
2166+
struct rb_class_cc_entries *ccs;
2167+
retry:
2168+
cc_tbl = RUBY_ATOMIC_VALUE_LOAD(RCLASS_WRITABLE_CC_TBL(klass));
2169+
ccs = NULL;
2170+
2171+
if (cc_tbl) {
2172+
// CCS data is keyed on method id, so we don't need the method id
2173+
// for doing comparisons in the `for` loop below.
2174+
2175+
if (rb_managed_id_table_lookup(cc_tbl, mid, (VALUE *)&ccs)) {
2176+
const int ccs_len = ccs->len;
2177+
2178+
if (UNLIKELY(METHOD_ENTRY_INVALIDATED(ccs->cme))) {
2179+
RB_VM_LOCKING() {
2180+
vm_evict_cc(klass, cc_tbl, mid);
2181+
}
2182+
goto retry;
2183+
}
2184+
else {
2185+
VM_ASSERT(vm_ccs_verify(ccs, mid, klass));
2186+
2187+
// We already know the method id is correct because we had
2188+
// to look up the ccs_data by method id. All we need to
2189+
// compare is argc and flag
2190+
unsigned int argc = vm_ci_argc(ci);
2191+
unsigned int flag = vm_ci_flag(ci);
2192+
2193+
for (int i=0; i<ccs_len; i++) {
2194+
unsigned int ccs_ci_argc = ccs->entries[i].argc;
2195+
unsigned int ccs_ci_flag = ccs->entries[i].flag;
2196+
const struct rb_callcache *ccs_cc = ccs->entries[i].cc;
2197+
2198+
VM_ASSERT(IMEMO_TYPE_P(ccs_cc, imemo_callcache));
2199+
2200+
if (ccs_ci_argc == argc && ccs_ci_flag == flag) {
2201+
RB_DEBUG_COUNTER_INC(cc_found_in_ccs);
2202+
2203+
VM_ASSERT(vm_cc_cme(ccs_cc)->called_id == mid);
2204+
VM_ASSERT(ccs_cc->klass == klass);
2205+
VM_ASSERT(!METHOD_ENTRY_INVALIDATED(vm_cc_cme(ccs_cc)));
2206+
2207+
return ccs_cc;
2208+
}
2209+
}
2210+
}
2211+
}
2212+
}
2213+
2214+
RB_GC_GUARD(cc_tbl);
2215+
return NULL;
2216+
}
2217+
2218+
static const struct rb_callcache *
2219+
vm_search_cc(const VALUE klass, const struct rb_callinfo * const ci)
2220+
{
2221+
const ID mid = vm_ci_mid(ci);
2222+
2223+
const struct rb_callcache *cc = vm_lookup_cc(klass, ci, mid);
2224+
if (cc) {
2225+
return cc;
2226+
}
2227+
2228+
RB_VM_LOCKING() {
2229+
if (rb_multi_ractor_p()) {
2230+
// The CC may have been populated by another ractor while we were waiting on the lock,
2231+
// so we must lookup a second time.
2232+
cc = vm_lookup_cc(klass, ci, mid);
2233+
}
2234+
2235+
if (!cc) {
2236+
cc = vm_populate_cc(klass, ci, mid);
2237+
}
2238+
}
2239+
21682240
return cc;
21692241
}
21702242

@@ -2175,16 +2247,14 @@ rb_vm_search_method_slowpath(const struct rb_callinfo *ci, VALUE klass)
21752247

21762248
VM_ASSERT_TYPE2(klass, T_CLASS, T_ICLASS);
21772249

2178-
RB_VM_LOCKING() {
2179-
cc = vm_search_cc(klass, ci);
2250+
cc = vm_search_cc(klass, ci);
21802251

2181-
VM_ASSERT(cc);
2182-
VM_ASSERT(IMEMO_TYPE_P(cc, imemo_callcache));
2183-
VM_ASSERT(cc == vm_cc_empty() || cc->klass == klass);
2184-
VM_ASSERT(cc == vm_cc_empty() || callable_method_entry_p(vm_cc_cme(cc)));
2185-
VM_ASSERT(cc == vm_cc_empty() || !METHOD_ENTRY_INVALIDATED(vm_cc_cme(cc)));
2186-
VM_ASSERT(cc == vm_cc_empty() || vm_cc_cme(cc)->called_id == vm_ci_mid(ci));
2187-
}
2252+
VM_ASSERT(cc);
2253+
VM_ASSERT(IMEMO_TYPE_P(cc, imemo_callcache));
2254+
VM_ASSERT(cc == vm_cc_empty() || cc->klass == klass);
2255+
VM_ASSERT(cc == vm_cc_empty() || callable_method_entry_p(vm_cc_cme(cc)));
2256+
VM_ASSERT(cc == vm_cc_empty() || !METHOD_ENTRY_INVALIDATED(vm_cc_cme(cc)));
2257+
VM_ASSERT(cc == vm_cc_empty() || vm_cc_cme(cc)->called_id == vm_ci_mid(ci));
21882258

21892259
return cc;
21902260
}

0 commit comments

Comments
 (0)