use inline cache for refinements

From Ruby 3.0, refined method invocations are slow because
resolved methods are not cached by inline cache because of
conservertive strategy. However, `using` clears all caches
so that it seems safe to cache resolved method entries.

This patch caches resolved method entries in inline cache
and clear all of inline method caches when `using` is called.

fix [Bug #18572]

```ruby
 # without refinements

class C
  def foo = :C
end

N = 1_000_000

obj = C.new
require 'benchmark'
Benchmark.bm{|x|
  x.report{N.times{
    obj.foo; obj.foo; obj.foo; obj.foo; obj.foo;
    obj.foo; obj.foo; obj.foo; obj.foo; obj.foo;
    obj.foo; obj.foo; obj.foo; obj.foo; obj.foo;
    obj.foo; obj.foo; obj.foo; obj.foo; obj.foo;
  }}
}

_END__
              user     system      total        real
master    0.362859   0.002544   0.365403 (  0.365424)
modified  0.357251   0.000000   0.357251 (  0.357258)
```

```ruby
 # with refinment but without using

class C
  def foo = :C
end

module R
  refine C do
    def foo = :R
  end
end

N = 1_000_000

obj = C.new
require 'benchmark'
Benchmark.bm{|x|
  x.report{N.times{
    obj.foo; obj.foo; obj.foo; obj.foo; obj.foo;
    obj.foo; obj.foo; obj.foo; obj.foo; obj.foo;
    obj.foo; obj.foo; obj.foo; obj.foo; obj.foo;
    obj.foo; obj.foo; obj.foo; obj.foo; obj.foo;
  }}
}
__END__
               user     system      total        real
master     0.957182   0.000000   0.957182 (  0.957212)
modified   0.359228   0.000000   0.359228 (  0.359238)
```

```ruby
 # with using

class C
  def foo = :C
end

module R
  refine C do
    def foo = :R
  end
end

N = 1_000_000

using R

obj = C.new
require 'benchmark'
Benchmark.bm{|x|
  x.report{N.times{
    obj.foo; obj.foo; obj.foo; obj.foo; obj.foo;
    obj.foo; obj.foo; obj.foo; obj.foo; obj.foo;
    obj.foo; obj.foo; obj.foo; obj.foo; obj.foo;
    obj.foo; obj.foo; obj.foo; obj.foo; obj.foo;
  }}
}
This commit is contained in:
Koichi Sasada 2023-07-31 16:17:55 +09:00
parent 280419d0e0
commit cfd7729ce7
Notes: git 2023-07-31 08:14:03 +00:00
7 changed files with 58 additions and 19 deletions

2
eval.c
View File

@ -1341,7 +1341,7 @@ rb_using_module(const rb_cref_t *cref, VALUE module)
{
Check_Type(module, T_MODULE);
using_module_recursive(cref, module);
rb_clear_method_cache_all();
rb_clear_all_refinement_method_cache();
}
/*

2
gc.c
View File

@ -3187,6 +3187,8 @@ vm_ccs_free(struct rb_class_cc_entries *ccs, int alive, rb_objspace_t *objspace,
asan_poison_object((VALUE)cc);
}
}
VM_ASSERT(!vm_cc_super_p(cc) && !vm_cc_refinement_p(cc));
vm_cc_invalidate(cc);
}
ruby_xfree(ccs->entries);

View File

@ -249,6 +249,6 @@ void rb_scope_visibility_set(rb_method_visibility_t);
VALUE rb_unnamed_parameters(int arity);
void rb_clear_method_cache(VALUE klass_or_module, ID mid);
void rb_clear_method_cache_all(void);
void rb_clear_all_refinement_method_cache(void);
#endif /* RUBY_METHOD_H */

View File

@ -2626,6 +2626,24 @@ class TestRefinement < Test::Unit::TestCase
assert_equal([], Refinement.used_modules)
end
def test_inlinecache
assert_separately([], <<-"end;")
module R
refine String do
def to_s = :R
end
end
2.times{|i|
s = ''.to_s
assert_equal '', s if i == 0
assert_equal :R, s if i == 1
using R if i == 0
assert_equal :R, ''.to_s
}
end;
end
private
def eval_using(mod, s)

View File

@ -299,10 +299,12 @@ struct rb_callcache {
#define VM_CALLCACHE_IVAR IMEMO_FL_USER0
#define VM_CALLCACHE_BF IMEMO_FL_USER1
#define VM_CALLCACHE_SUPER IMEMO_FL_USER2
#define VM_CALLCACHE_REFINEMENT IMEMO_FL_USER3
enum vm_cc_type {
cc_type_normal, // chained from ccs
cc_type_super,
cc_type_refinement,
};
extern const struct rb_callcache *rb_vm_empty_cc(void);
@ -332,6 +334,9 @@ vm_cc_new(VALUE klass,
case cc_type_super:
*(VALUE *)&cc->flags |= VM_CALLCACHE_SUPER;
break;
case cc_type_refinement:
*(VALUE *)&cc->flags |= VM_CALLCACHE_REFINEMENT;
break;
}
vm_cc_attr_index_initialize(cc, INVALID_SHAPE_ID);
@ -345,6 +350,12 @@ vm_cc_super_p(const struct rb_callcache *cc)
return (cc->flags & VM_CALLCACHE_SUPER) != 0;
}
static inline bool
vm_cc_refinement_p(const struct rb_callcache *cc)
{
return (cc->flags & VM_CALLCACHE_REFINEMENT) != 0;
}
#define VM_CC_ON_STACK(clazz, call, aux, cme) \
(struct rb_callcache) { \
.flags = T_IMEMO | \

View File

@ -2003,6 +2003,8 @@ vm_ccs_verify(struct rb_class_cc_entries *ccs, ID mid, VALUE klass)
VM_ASSERT(IMEMO_TYPE_P(cc, imemo_callcache));
VM_ASSERT(vm_cc_class_check(cc, klass));
VM_ASSERT(vm_cc_check_cme(cc, ccs->cme));
VM_ASSERT(!vm_cc_super_p(cc));
VM_ASSERT(!vm_cc_refinement_p(cc));
}
return TRUE;
}
@ -4193,13 +4195,20 @@ search_refined_method(rb_execution_context_t *ec, rb_control_frame_t *cfp, struc
static VALUE
vm_call_refined(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling)
{
struct rb_callcache *ref_cc = &VM_CC_ON_STACK(Qundef, vm_call_general, {{ 0 }},
search_refined_method(ec, cfp, calling));
const rb_callable_method_entry_t *ref_cme = search_refined_method(ec, cfp, calling);
if (vm_cc_cme(ref_cc)) {
if (ref_cme) {
if (calling->cd->cc) {
const struct rb_callcache *cc = calling->cc = vm_cc_new(vm_cc_cme(calling->cc)->defined_class, ref_cme, vm_call_general, cc_type_refinement);
RB_OBJ_WRITE(cfp->iseq, &calling->cd->cc, cc);
return vm_call_method(ec, cfp, calling);
}
else {
struct rb_callcache *ref_cc = &VM_CC_ON_STACK(Qundef, vm_call_general, {{ 0 }}, ref_cme);
calling->cc= ref_cc;
return vm_call_method(ec, cfp, calling);
}
}
else {
return vm_call_method_nome(ec, cfp, calling);
}

View File

@ -307,19 +307,19 @@ rb_clear_method_cache(VALUE klass_or_module, ID mid)
void rb_cc_table_free(VALUE klass);
static int
invalidate_all_cc(void *vstart, void *vend, size_t stride, void *data)
invalidate_all_refinement_cc(void *vstart, void *vend, size_t stride, void *data)
{
VALUE v = (VALUE)vstart;
for (; v != (VALUE)vend; v += stride) {
void *ptr = asan_poisoned_object_p(v);
asan_unpoison_object(v, false);
if (RBASIC(v)->flags) { // liveness check
if (RB_TYPE_P(v, T_CLASS) ||
RB_TYPE_P(v, T_ICLASS)) {
if (RCLASS_CC_TBL(v)) {
rb_cc_table_free(v);
if (imemo_type_p(v, imemo_callcache)) {
const struct rb_callcache *cc = (const struct rb_callcache *)v;
if (vm_cc_refinement_p(cc) && cc->klass) {
vm_cc_invalidate(cc);
}
RCLASS_CC_TBL(v) = NULL;
}
}
@ -331,10 +331,9 @@ invalidate_all_cc(void *vstart, void *vend, size_t stride, void *data)
}
void
rb_clear_method_cache_all(void)
rb_clear_all_refinement_method_cache(void)
{
rb_objspace_each_objects(invalidate_all_cc, NULL);
rb_objspace_each_objects(invalidate_all_refinement_cc, NULL);
rb_yjit_invalidate_all_method_lookup_assumptions();
}