More indy perf anecdotes

Charles Oliver Nutter headius at headius.com
Sun Apr 10 15:18:51 PDT 2011


Today I played a bit more with expanding our use of invokedynamic in
JRuby. The experiment today was to replace direct guarded calls with
indy calls.

Specifically, when calling an operator (+, -, <, etc) with a literal
integer, we use a different call site that checks instanceof
RubyFixnum and calls the actual method (op_plus, op_minus, op_lt,
etc). So there's a monomorphic path as long as the target is also a
RubyFixnum. code => PlusCallSite (for example) => RubyFixnum.op_plus.

Replacing this with indy uses a guardWithTest where the test is "self
instanceof RubyFixnum", and the target path binds directly through to
the RubyFixnum method with a bit of argument dropping.

This is a good test for indy perf versus the same logic done with
instance, casts, and invokevirtual.

With the previous logic, fib(35) settles in around 0.74s. With the
indy logic, performance decreases to 0.83s or so.

This does beat logic that dispatched operators with literal values
like any other, passing a RubyFixnum object and doing a full type
identity and modification guard...that comes in closer to 0.95 or
worse. Fixnum math operations really beg for as direct logic as
possible, and it looks like indy adds a healthy amount of overhead
over all virtual invocations.

I have not done any investigation into the resulting assembly yet.

Here's the original logic for +, in PlusCallSite (bytecode emitted
calls virtual method CallSite.call):

   public IRubyObject call(ThreadContext context, IRubyObject caller,
IRubyObject self, long fixnum) {
        if (self instanceof RubyFixnum) {
            return ((RubyFixnum) self).op_plus(context, fixnum);
        } else if (self instanceof RubyFloat) {
            return ((RubyFloat) self).op_plus(context, fixnum);
        }
        return super.call(context, caller, self, fixnum);
    }

Here's how the indy logic does it:

ASM emission:
    public void invokeBinaryFixnumRHS(String name, CompilerCallback
receiverCallback, long fixnum) {
        if (receiverCallback != null) {
            receiverCallback.call(methodCompiler);
        } else {
            methodCompiler.loadSelf();
        }
        methodCompiler.loadThreadContext();
        method.ldc(name);
        method.ldc(fixnum);

        String signature = sig(IRubyObject.class,
params(IRubyObject.class, ThreadContext.class, String.class,
long.class));

        method.invokedynamic("fixnumOperator", signature,
InvokeDynamicSupport.bootstrapHandle("bootstrapOperatorFixnum"));
    }

Bootstrap:
    public static CallSite
bootstrapOperatorFixnum(MethodHandles.Lookup lookup, String name,
MethodType type) throws NoSuchMethodException, IllegalAccessException
{
        CallSite site = new MutableCallSite(type);

        MethodType fallbackType = type.insertParameterTypes(0,
MutableCallSite.class);
        MethodHandle myFallback = MethodHandles.insertArguments(
                lookup.findStatic(InvokeDynamicSupport.class, "fixnumFallback",
                fallbackType),
                0,
                site);
        site.setTarget(myFallback);
        return site;
    }

Fallback, test, and slow-path (when a non-Fixnum is encountered) logic:
    public static IRubyObject fixnumFallback(MutableCallSite site,
IRubyObject self, ThreadContext context, String name, long value)
throws Throwable {
        RubyFixnum fixnum = context.runtime.newFixnum(value);
        if (self instanceof RubyFixnum) {
            String realMethod = MethodIndex.getFastOpsMethod(name);
            if (realMethod != null) {
                try {
                    MethodHandle direct =
MethodHandles.lookup().findVirtual(RubyFixnum.class, realMethod,
MethodType.methodType(IRubyObject.class, ThreadContext.class,
long.class));
                    MethodHandle target =
MethodHandles.explicitCastArguments(direct,
MethodType.methodType(IRubyObject.class, IRubyObject.class,
ThreadContext.class, long.class));
                    target = MethodHandles.dropArguments(target, 2,
String.class);

                    MethodHandle test =
MethodHandles.lookup().findStatic(InvokeDynamicSupport.class,
"fixnumTest", site.type().changeReturnType(boolean.class));

                    MethodType fallbackType =
site.type().insertParameterTypes(0, MutableCallSite.class);
                    MethodHandle fallback = MethodHandles.insertArguments(

MethodHandles.lookup().findStatic(InvokeDynamicSupport.class,
"fixnumFallback",
                            fallbackType),
                            0,
                            site);

                    site.setTarget(MethodHandles.guardWithTest(test,
target, fallback));
                    return self.callMethod(context, name, fixnum);
                } catch (NoSuchMethodException nsme) {
                    // just fall through
                }
            }
        } else {
            MethodType slowType = site.type().insertParameterTypes(0,
MutableCallSite.class);
            MethodHandle target = MethodHandles.insertArguments(

MethodHandles.lookup().findStatic(InvokeDynamicSupport.class,
"fixnumSlow",
                    slowType),
                    0,
                    fixnum);
            site.setTarget(target);
        }

        return self.callMethod(context, name, fixnum);
    }

    public static boolean fixnumTest(IRubyObject self, ThreadContext
context, String name, long value) {
        return self instanceof RubyFixnum;
    }

    public static IRubyObject fixnumSlow(IRubyObject fixValue,
IRubyObject self, ThreadContext context, String name, long value)
throws Throwable {
        return self.callMethod(context, name, fixValue);
    }

- Charlie


More information about the mlvm-dev mailing list