Skip to content

Implement fast-path builtin method flags #9119

Description

@headius

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:

  1. get the call sites from context
  2. access the Fixnum sites
  3. get the equality site object from Fixnum
  4. get the metaclass from the object
  5. get the cache object from the site
  6. confirm the cache is still valid (accesses a generation count on the class)
  7. get the method from the cache object
  8. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions