Skip to content

Commit 15717eb

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 6f862e0 commit 15717eb

5 files changed

Lines changed: 191 additions & 58 deletions

File tree

benchmark/vm_cc_lookup.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
prelude: |
2+
class A
3+
def test
4+
2 + 2
5+
end
6+
end
7+
8+
class B
9+
def test
10+
1 + 1
11+
end
12+
end
13+
14+
def call_test(obj)
15+
obj.test
16+
end
17+
18+
A_INSTANCE = A.new
19+
B_INSTANCE = B.new
20+
21+
Warning[:experimental] = false
22+
rac = Ractor.new { }
23+
24+
benchmark:
25+
inline_cc_miss: |
26+
call_test(A_INSTANCE)
27+
call_test(B_INSTANCE)

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);

vm_callinfo.h

Lines changed: 2 additions & 0 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,

vm_insnhelper.c

Lines changed: 125 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,89 @@ 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 = 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+
return NULL;
2214+
}
2215+
2216+
static const struct rb_callcache *
2217+
vm_search_cc(const VALUE klass, const struct rb_callinfo * const ci)
2218+
{
2219+
const ID mid = vm_ci_mid(ci);
2220+
2221+
const struct rb_callcache *cc = vm_lookup_cc(klass, ci, mid);
2222+
if (cc) {
2223+
return cc;
2224+
}
2225+
2226+
RB_VM_LOCKING() {
2227+
if (rb_multi_ractor_p()) {
2228+
// The CC may have been populated by another ractor while we were waiting on the lock,
2229+
// so we must lookup a second time.
2230+
cc = vm_lookup_cc(klass, ci, mid);
2231+
}
2232+
2233+
if (!cc) {
2234+
cc = vm_populate_cc(klass, ci, mid);
2235+
}
2236+
}
2237+
21682238
return cc;
21692239
}
21702240

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

21762246
VM_ASSERT_TYPE2(klass, T_CLASS, T_ICLASS);
21772247

2178-
RB_VM_LOCKING() {
2179-
cc = vm_search_cc(klass, ci);
2248+
cc = vm_search_cc(klass, ci);
21802249

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-
}
2250+
VM_ASSERT(cc);
2251+
VM_ASSERT(IMEMO_TYPE_P(cc, imemo_callcache));
2252+
VM_ASSERT(cc == vm_cc_empty() || cc->klass == klass);
2253+
VM_ASSERT(cc == vm_cc_empty() || callable_method_entry_p(vm_cc_cme(cc)));
2254+
VM_ASSERT(cc == vm_cc_empty() || !METHOD_ENTRY_INVALIDATED(vm_cc_cme(cc)));
2255+
VM_ASSERT(cc == vm_cc_empty() || vm_cc_cme(cc)->called_id == vm_ci_mid(ci));
21882256

21892257
return cc;
21902258
}

vm_method.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,42 @@ rb_vm_cc_table_create(size_t capa)
142142
return rb_managed_id_table_create(&cc_table_type, capa);
143143
}
144144

145+
static enum rb_id_table_iterator_result
146+
vm_cc_table_dup_i(ID key, VALUE old_ccs_ptr, void *data)
147+
{
148+
struct rb_class_cc_entries *old_ccs = (struct rb_class_cc_entries *)old_ccs_ptr;
149+
struct rb_class_cc_entries *new_ccs = ALLOC(struct rb_class_cc_entries);
150+
MEMCPY(new_ccs, old_ccs, struct rb_class_cc_entries, 1);
151+
new_ccs->entries = ALLOC_N(struct rb_class_cc_entries_entry, new_ccs->capa);
152+
MEMCPY(&new_ccs->entries, &old_ccs->entries, struct rb_class_cc_entries_entry, new_ccs->capa);
153+
154+
VALUE new_table = (VALUE)data;
155+
rb_managed_id_table_insert(new_table, key, (VALUE)new_ccs);
156+
for (int index = 0; index < new_ccs->len; index++) {
157+
RB_OBJ_WRITTEN(new_table, Qundef, new_ccs->entries[index].cc);
158+
}
159+
return ID_TABLE_CONTINUE;
160+
}
161+
162+
VALUE
163+
rb_vm_cc_table_dup(VALUE old_table)
164+
{
165+
VALUE new_table = rb_vm_cc_table_create(rb_managed_id_table_size(old_table));
166+
rb_managed_id_table_foreach(old_table, vm_cc_table_dup_i, (void *)new_table);
167+
return new_table;
168+
}
169+
170+
void
171+
rb_vm_cc_table_delete(VALUE table, ID mid)
172+
{
173+
struct rb_class_cc_entries *ccs;
174+
if (rb_managed_id_table_lookup(table, mid, (VALUE *)&ccs)) {
175+
// FIXME: What are the invalidation considerations? e.g. invalidate_method_cache_in_cc_table?
176+
rb_managed_id_table_delete(table, mid);
177+
rb_vm_ccs_free(ccs);
178+
}
179+
}
180+
145181
static enum rb_id_table_iterator_result
146182
vm_ccs_dump_i(ID mid, VALUE val, void *data)
147183
{

0 commit comments

Comments
 (0)