Don't create CCs for methods we assume are cold

The assumption is that we're usually calling these methods on boot, and they don't
need CCs. CCs scale with the number of classes that use these methods in the app, so
if you have 10k classes and these classes use 3 of these methods on average, then that
saves 30k CC objects.

Methods on Module that are marked cold are:

public, protected, private, module_function, ruby2_keywords, alias_method, undef_method,
remove_method, attr_reader, attr_writer, attr_accessor, include, prepend.

If these methods ever get redefined, they get new method definitions that are not
marked uncacheable (CCs will get created).

In one of our apps (not the largest), we observed this difference after boot:

| method            | before | after | delta       |
|-------------------|-------:|------:|------------:|
| `include`         |   4518 |     2 |       -4516 |
| `alias_method`    |   3921 |     0 |       -3921 |
| `ruby2_keywords`  |   3784 |     0 |       -3784 |
| `private`         |   3519 |     3 |       -3516 |
| `attr_reader`     |   2738 |     0 |       -2738 |
| `attr_accessor`   |   2288 |     0 |       -2288 |
| `prepend`         |    354 |     1 |        -353 |
| `protected`       |    303 |     0 |        -303 |
| `public`          |    302 |     0 |        -302 |
| `undef_method`    |    178 |     0 |        -178 |
| `attr_writer`     |    124 |     0 |        -124 |
| `module_function` |     42 |     0 |         -42 |
| `remove_method`   |     24 |     0 |         -24 |
| **total**         |        |       | **-22,089** |

NOTE: CC tables and ccs (the malloced struct) are still being created, so this only reduces
the CC objects themselves. I will tackle that in a follow-up.
This commit is contained in:
Luke Gruber authored and Luke Gruber committed 2026-10-27 09:49:20 -04:00
1 parent 32f06ce98c
commit ae2c6841c4
9 files changed
+127 -7
Notes: git 2026-10-27 13:49:57 +00:00

No files matched your search

+1
View File
@@ -107,6 +107,7 @@ RB_DEBUG_COUNTER(ccf_iseq_bmethod)
RB_DEBUG_COUNTER(ccf_noniseq_bmethod)
RB_DEBUG_COUNTER(ccf_opt_send_complex)
RB_DEBUG_COUNTER(ccf_opt_send_simple)
RB_DEBUG_COUNTER(ccf_uncached)
/*
* control frame push counts.
+1
View File
@@ -55,6 +55,7 @@ rb_call_inits(void)
CALL(Ruby_module);
CALL(Box);
CALL(Proc);
CALL(uncached_methods); /* after every uncached method is defined */
CALL(Binding);
CALL(Math);
CALL(GC);
+2 -1
View File
@@ -433,7 +433,8 @@ rb_iseq_mark_and_move(const struct rb_gc_mark_ctx *ctx, rb_iseq_t *iseq, bool re
if (cds[i].ci) rb_gc_mark_and_move_ptr_ctx(ctx, &cds[i].ci);
const struct rb_callcache *cc = cds[i].cc;
if (!cc || cc == rb_vm_empty_cc() || cc == rb_vm_empty_cc_for_super()) {
if (!cc || cc == rb_vm_empty_cc() || cc == rb_vm_empty_cc_for_super() ||
cc == rb_vm_uncached_cc()) {
// No need for marking, reference updating, or clearing
// We also want to avoid reassigning to improve CoW
continue;
+1
View File
@@ -189,6 +189,7 @@ struct rb_method_definition_struct {
unsigned int iseq_overload: 1;
unsigned int no_redef_warning: 1;
unsigned int aliased : 1;
unsigned int uncached : 1;
rb_atomic_t reference_count;
+19
View File
@@ -781,6 +781,19 @@ static const struct rb_callcache vm_empty_cc = {
}
};
/* Installed instead of a real cc at call sites of methods that opted out of
* call caching: dispatch resolves the cme on every call rather than leaving a
* cc behind on the receiver's class. */
static const struct rb_callcache vm_uncached_cc = {
.flags = T_IMEMO | (imemo_callcache << FL_USHIFT) | VM_CALLCACHE_UNMARKABLE,
.klass = Qundef,
.cme_ = NULL,
.call_ = vm_call_uncached,
.aux_ = {
.v = Qfalse,
}
};
static const struct rb_callcache vm_empty_cc_for_super = {
.flags = T_IMEMO | (imemo_callcache << FL_USHIFT) | VM_CALLCACHE_UNMARKABLE,
.klass = Qundef,
@@ -5463,4 +5476,10 @@ rb_vm_empty_cc_for_super(void)
return &vm_empty_cc_for_super;
}
const struct rb_callcache *
rb_vm_uncached_cc(void)
{
return &vm_uncached_cc;
}
#include "vm_call_iseq_optimized.inc" /* required from vm_insnhelper.c */
+1
View File
@@ -326,6 +326,7 @@ enum vm_cc_type {
extern const struct rb_callcache *rb_vm_empty_cc(void);
extern const struct rb_callcache *rb_vm_empty_cc_for_super(void);
extern const struct rb_callcache *rb_vm_uncached_cc(void);
#define vm_cc_empty() rb_vm_empty_cc()
+24
View File
@@ -420,6 +420,11 @@ gccct_method_search_slowpath(rb_vm_t *vm, VALUE klass, unsigned int index, const
vm_search_method_slowpath0(vm->self, &cd, klass);
if (UNLIKELY(cd.cc == rb_vm_uncached_cc())) {
// Carries no cme, so it would only ever miss here. Don't evict a usable entry for it.
return cd.cc;
}
if (UNLIKELY(!vm->global_cc_cache_table_used)) {
vm->global_cc_cache_table_used = true;
}
@@ -524,6 +529,21 @@ rb_gccct_clear_table(void)
*
* @note `self` is used in order to controlling access to protected methods.
*/
static VALUE
vm_call0_uncached(rb_execution_context_t *ec, VALUE recv, ID mid, int argc, const VALUE *argv,
int kw_splat, call_type scope, VALUE self)
{
const rb_callable_method_entry_t *cme = rb_callable_method_entry(CLASS_OF(recv), mid);
enum method_missing_reason call_status = rb_method_call_status(ec, cme, scope, self);
if (UNLIKELY(call_status != MISSING_NONE)) {
return method_missing(ec, recv, mid, argc, argv, call_status, kw_splat);
}
stack_check(ec);
return rb_vm_call0(ec, recv, mid, argc, argv, cme, kw_splat);
}
static inline VALUE
rb_call0(rb_execution_context_t *ec,
VALUE recv, ID mid, int argc, const VALUE *argv,
@@ -551,6 +571,10 @@ rb_call0(rb_execution_context_t *ec,
const struct rb_callcache *cc = gccct_method_search(ec, recv, mid, &ci);
if (UNLIKELY(cc == rb_vm_uncached_cc())) {
return vm_call0_uncached(ec, recv, mid, argc, argv, kw_splat, scope, self);
}
if (scope == CALL_PUBLIC) {
RB_DEBUG_COUNTER_INC(call0_public);
+52 -6
View File
@@ -44,6 +44,7 @@ extern VALUE rb_make_no_method_exception(VALUE exc, VALUE format, VALUE obj,
static const struct rb_callcache vm_empty_cc;
static const struct rb_callcache vm_empty_cc_for_super;
static const struct rb_callcache vm_uncached_cc;
/* control stack frame */
@@ -2113,6 +2114,14 @@ vm_populate_cc(VALUE klass, const struct rb_callinfo * const ci, ID mid)
return &vm_empty_cc;
}
if (UNLIKELY(cme->def->uncached && !(vm_ci_flag(ci) & VM_CALL_SUPER))) {
// Leaving a cc here would cost a ccs and a cc table entry on klass, which
// is usually a metaclass, so the cost would scale with the number of
// classes. invokesuper is excluded because vm_search_super_method needs a
// cme in the cc it gets back.
return &vm_uncached_cc;
}
VALUE cc_tbl = RCLASS_WRITABLE_CC_TBL(klass);
const VALUE original_cc_table = cc_tbl;
if (!cc_tbl) {
@@ -2246,6 +2255,14 @@ vm_search_cc(const VALUE klass, const struct rb_callinfo * const ci)
return cc;
}
#if VM_CHECK_MODE > 0
static bool
vm_cc_resolved_p(const struct rb_callcache *cc)
{
return cc != &vm_empty_cc && cc != &vm_uncached_cc;
}
#endif
const struct rb_callcache *
rb_vm_search_method_slowpath(const struct rb_callinfo *ci, VALUE klass)
{
@@ -2269,10 +2286,10 @@ rb_vm_search_method_slowpath(const struct rb_callinfo *ci, VALUE klass)
VM_ASSERT(cc);
VM_ASSERT(IMEMO_TYPE_P(cc, imemo_callcache));
VM_ASSERT(cc == vm_cc_empty() || cc->klass == klass);
VM_ASSERT(cc == vm_cc_empty() || callable_method_entry_p(vm_cc_cme(cc)));
VM_ASSERT(cc == vm_cc_empty() || !METHOD_ENTRY_INVALIDATED(vm_cc_cme(cc)));
VM_ASSERT(cc == vm_cc_empty() || vm_cc_cme(cc)->called_id == vm_ci_mid(ci));
VM_ASSERT(!vm_cc_resolved_p(cc) || cc->klass == klass);
VM_ASSERT(!vm_cc_resolved_p(cc) || callable_method_entry_p(vm_cc_cme(cc)));
VM_ASSERT(!vm_cc_resolved_p(cc) || !METHOD_ENTRY_INVALIDATED(vm_cc_cme(cc)));
VM_ASSERT(!vm_cc_resolved_p(cc) || vm_cc_cme(cc)->called_id == vm_ci_mid(ci));
return cc;
}
@@ -2300,8 +2317,7 @@ vm_search_method_slowpath0(VALUE cd_owner, struct rb_call_data *cd, VALUE klass)
#if OPT_INLINE_METHOD_CACHE
cd->cc = cc;
const struct rb_callcache *empty_cc = &vm_empty_cc;
if (cd_owner && cc != empty_cc) {
if (cd_owner && vm_cc_markable(cc)) {
// invokesuper dispatches with a cd on the machine stack, which has no
// owning iseq to remember and keeps its ci out of the GC.
VM_ASSERT(!vm_ci_markable(cd->ci) ||
@@ -2310,6 +2326,7 @@ vm_search_method_slowpath0(VALUE cd_owner, struct rb_call_data *cd, VALUE klass)
}
#if USE_DEBUG_COUNTER
const struct rb_callcache *empty_cc = &vm_empty_cc;
if (!old_cc || old_cc == empty_cc) {
// empty
RB_DEBUG_COUNTER_INC(mc_inline_miss_empty);
@@ -2369,6 +2386,9 @@ vm_search_method_fastpath(const struct rb_control_frame_struct *reg_cfp, struct
if (vm_cc_hit_p(cc, cd, klass)) {
return cc;
}
if (UNLIKELY(cc == &vm_uncached_cc)) {
return cc;
}
return vm_search_method_slowpath0((VALUE)CFP_ISEQ(reg_cfp), cd, klass);
}
@@ -2384,6 +2404,29 @@ vm_search_method(struct rb_control_frame_struct *reg_cfp, struct rb_call_data *c
return vm_cc_cme(cc);
}
static VALUE
vm_call_uncached(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling)
{
RB_DEBUG_COUNTER_INC(ccf_uncached);
const struct rb_call_data *cd = calling->cd;
const VALUE klass = CLASS_OF(calling->recv);
const rb_callable_method_entry_t *cme = rb_callable_method_entry(klass, vm_ci_mid(cd->ci));
if (UNLIKELY(cme == NULL || !cme->def->uncached)) {
// A site that saw an uncached method is now seeing a receiver whose
// method is cacheable, so let it have a real cc after all.
const struct rb_callcache *cc =
vm_search_method_slowpath0((VALUE)CFP_ISEQ(cfp), (struct rb_call_data *)cd, klass);
calling->cc = cc;
return vm_cc_call(cc)(ec, cfp, calling);
}
calling->cc = &VM_CC_ON_STACK(klass, vm_call_general, {{ 0 }},
rb_check_overloaded_cme(cme, cd->ci));
return vm_call_general(ec, cfp, calling);
}
const struct rb_callable_method_entry_struct *
rb_zjit_vm_search_method(VALUE cd_owner, struct rb_call_data *cd, VALUE recv)
{
@@ -2392,6 +2435,9 @@ rb_zjit_vm_search_method(VALUE cd_owner, struct rb_call_data *cd, VALUE recv)
VALUE klass = CLASS_OF(recv);
const struct rb_callcache *cc = cd->cc;
if (!vm_cc_hit_p(cc, cd, klass)) {
if (UNLIKELY(cc == &vm_uncached_cc)) {
return rb_callable_method_entry(klass, vm_ci_mid(cd->ci));
}
cc = vm_search_method_slowpath0(cd_owner, cd, klass);
}
return vm_cc_cme(cc);
+26
View File
@@ -3776,6 +3776,32 @@ obj_respond_to_missing(VALUE obj, VALUE mid, VALUE priv)
return Qfalse;
}
static void
method_uncached(VALUE klass, const char *name)
{
rb_method_entry_t *me = lookup_method_table(klass, rb_intern(name));
VM_ASSERT(me != NULL);
me->def->uncached = 1;
}
/* Called a fixed number of times per class, from a class body, so a cc would be
* a permanent cost per class for no reuse. */
void
Init_uncached_methods(void)
{
static const char *const names[] = {
"public", "protected", "private", "module_function", "ruby2_keywords",
"alias_method", "undef_method", "remove_method",
"attr_reader", "attr_writer", "attr_accessor",
"include", "prepend",
};
for (int i = 0; i < numberof(names); i++) {
method_uncached(rb_cModule, names[i]);
}
}
void
Init_eval_method(void)
{