There are many places where we have fast path logic we can call if a given object is a core type with its original builtin method. For example, if we are checking equality of two Integer instances, and nobody has redefined Integer#==, we can simply compare the value they hold rather than dynamically dispatching.
CRuby does this via a global set of bit flags.
The storage of those flags looks like this:
RUBY_EXTERN short ruby_vm_redefined_flag[BOP_LAST_];
The bits in these slots are set by a combination of methods and classes that are frequently checked for redefinition. Any location in the codebase that wants to short-circuit a slow dynamic call to one of these classes and methods has only a single memory address to load.
In JRuby, we do this by using call sites and the isBuiltin flag on DynamicMethod. While this works, and helps us avoid doing a full dynamic call for those methods, there's a huge amount of work in comparison:
Getting to the isBuiltin call:
if (!context.sites.Fixnum.op_eqq.isBuiltin(a)) {
And the builtin check:
public boolean isBuiltin(IRubyObject self) {
RubyClass selfType = getMetaClass(self);
// This must be retrieved *once* to avoid racing with other threads.
CacheEntry cache = this.cache;
if (cache.typeOk(selfType)) {
return cache.method.isBuiltin();
}
return cacheAndGet(self, selfType, methodName).method.isBuiltin(); // false for method.isUndefined()
}
So a rough list of memory accesses:
- get the call sites from context
- access the Fixnum sites
- get the equality site object from Fixnum
- get the metaclass from the object
- get the cache object from the site
- confirm the cache is still valid (accesses a generation count on the class)
- get the method from the cache object
- call isBuiltin (which accesses a boolean on
DynamicMethod)
That's 8x more memory accesses than in CRuby and obviously terrible for cache locality. It's also frequently done in a loop calling equalInternal with none of these references cached between calls.
I think we should implement a similar BuiltinMethods object that can store these bits in a similar way and be passed on the call stack with context. We would then replace the above check with something like:
if (context.builtins.builtinIntegerEquals()) { // fast path
It's not a single access, but it's 4x fewer.
See #9116 for an example of this in practice that greatly speeds up a loop-heavy method.
There are many places where we have fast path logic we can call if a given object is a core type with its original builtin method. For example, if we are checking equality of two
Integerinstances, and nobody has redefinedInteger#==, we can simply compare the value they hold rather than dynamically dispatching.CRuby does this via a global set of bit flags.
The storage of those flags looks like this:
The bits in these slots are set by a combination of methods and classes that are frequently checked for redefinition. Any location in the codebase that wants to short-circuit a slow dynamic call to one of these classes and methods has only a single memory address to load.
In JRuby, we do this by using call sites and the
isBuiltinflag onDynamicMethod. While this works, and helps us avoid doing a full dynamic call for those methods, there's a huge amount of work in comparison:Getting to the
isBuiltincall:And the builtin check:
So a rough list of memory accesses:
DynamicMethod)That's 8x more memory accesses than in CRuby and obviously terrible for cache locality. It's also frequently done in a loop calling
equalInternalwith none of these references cached between calls.I think we should implement a similar
BuiltinMethodsobject that can store these bits in a similar way and be passed on the call stack withcontext. We would then replace the above check with something like:It's not a single access, but it's 4x fewer.
See #9116 for an example of this in practice that greatly speeds up a loop-heavy method.