mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 08:25:20 +08:00
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:
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
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 */
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user