Skip to content

Commit

Permalink
Permute indy invoke paths to reduce uncached stack
Browse files Browse the repository at this point in the history
Passing all uncached calls through the varargs form simplifies
maintenance, but requires an indirect dispatch for the first
invocation. This adds several frames to the stack for such uncached
calls. The patch here permutes the InvokeSite.invoke methods so
they can directly dispatch to the to-be-cached handle, reducing the
number of stack frames in a backtrace.

This is unlikely to reduce actual stack consumption due to the
unoptimized MethodHandle and LambdaForm between the invokeExact
call and the target method.
  • Loading branch information
headius committed Nov 22, 2024
1 parent 637a990 commit b9f5b2a
Showing 1 changed file with 268 additions and 56 deletions.
324 changes: 268 additions & 56 deletions core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
import static java.lang.invoke.MethodType.methodType;
import static org.jruby.runtime.Helpers.arrayOf;
import static org.jruby.runtime.Helpers.constructObjectArrayHandle;
import static org.jruby.runtime.Helpers.throwException;
import static org.jruby.runtime.invokedynamic.JRubyCallSite.SITE_ID;

/**
Expand Down Expand Up @@ -625,7 +626,7 @@ public InvokeSite(MethodType type, String name, CallType callType, boolean liter

this.arity = arity;

this.fallback = prepareBinder(true).invokeVirtualQuiet(LOOKUP, "invoke");
this.fallback = prepareBinder(true).invokeVirtualQuiet(LOOKUP, functional ? "finvoke" : "invoke");
}

public static CallSite bootstrap(InvokeSite site, MethodHandles.Lookup lookup) {
Expand All @@ -635,37 +636,278 @@ public static CallSite bootstrap(InvokeSite site, MethodHandles.Lookup lookup) {
}

public IRubyObject invoke(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject[] args, Block block) throws Throwable {
RubyClass selfClass = pollAndGetClass(context, self);
SwitchPoint switchPoint = (SwitchPoint) selfClass.getInvalidator().getData();
String methodName = this.methodName;
CacheEntry entry = selfClass.searchWithCache(methodName);
MethodHandle mh;
boolean passSymbol = false;
Call call = prepareCall(context, caller, self);

if (methodMissing(entry, caller)) {
entry = methodMissingEntry(context, selfClass, methodName, entry);
// only pass symbol below if we be calling a user-defined method_missing (default ones do it for us)
passSymbol = !(entry.method instanceof RubyKernel.MethodMissingMethod ||
entry.method instanceof Helpers.MethodMissingWrapper);
mh = buildGenericHandle(entry);
} else {
mh = getHandle(self, entry);
finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, caller, self, args, block);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject invoke(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject[] args) throws Throwable {
Call call = prepareCall(context, caller, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, caller, self, args);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject finvoke(ThreadContext context, IRubyObject self, IRubyObject[] args, Block block) throws Throwable {
Call call = prepareCall(context, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, self, args, block);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject finvoke(ThreadContext context, IRubyObject self, IRubyObject[] args) throws Throwable {
Call call = prepareCall(context, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, self, args);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject invoke(ThreadContext context, IRubyObject caller, IRubyObject self, Block block) throws Throwable {
Call call = prepareCall(context, caller, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, caller, self, block);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject invoke(ThreadContext context, IRubyObject caller, IRubyObject self) throws Throwable {
Call call = prepareCall(context, caller, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, caller, self);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject finvoke(ThreadContext context, IRubyObject self, Block block) throws Throwable {
Call call = prepareCall(context, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, self, block);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject finvoke(ThreadContext context, IRubyObject self) throws Throwable {
Call call = prepareCall(context, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, self);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject invoke(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject arg0, Block block) throws Throwable {
Call call = prepareCall(context, caller, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, caller, self, arg0, block);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject invoke(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject arg0) throws Throwable {
Call call = prepareCall(context, caller, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, caller, self, arg0);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject finvoke(ThreadContext context, IRubyObject self, IRubyObject arg0, Block block) throws Throwable {
Call call = prepareCall(context, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, self, arg0, block);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject finvoke(ThreadContext context, IRubyObject self, IRubyObject arg0) throws Throwable {
Call call = prepareCall(context, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, self, arg0);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject invoke(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject arg0, IRubyObject arg1, Block block) throws Throwable {
Call call = prepareCall(context, caller, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, caller, self, arg0, arg1, block);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject invoke(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject arg0, IRubyObject arg1) throws Throwable {
Call call = prepareCall(context, caller, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, caller, self, arg0, arg1);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject finvoke(ThreadContext context, IRubyObject self, IRubyObject arg0, IRubyObject arg1, Block block) throws Throwable {
Call call = prepareCall(context, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, self, arg0, arg1, block);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject finvoke(ThreadContext context, IRubyObject self, IRubyObject arg0, IRubyObject arg1) throws Throwable {
Call call = prepareCall(context, self);

finishBinding(entry, mh, self, selfClass, switchPoint);
finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

return performIndirectCall(context, self, args, block, methodName, passSymbol, entry);
try {
return (IRubyObject) call.handle.invokeExact(context, self, arg0, arg1);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject invoke(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) throws Throwable {
Call call = prepareCall(context, caller, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, caller, self, arg0, arg1, arg2, block);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject invoke(ThreadContext context, IRubyObject self, IRubyObject[] args, Block block) throws Throwable {
public IRubyObject invoke(ThreadContext context, IRubyObject caller, IRubyObject self, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2) throws Throwable {
Call call = prepareCall(context, caller, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, caller, self, arg0, arg1, arg2);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject finvoke(ThreadContext context, IRubyObject self, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) throws Throwable {
Call call = prepareCall(context, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, self, arg0, arg1, arg2, block);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

public IRubyObject finvoke(ThreadContext context, IRubyObject self, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2) throws Throwable {
Call call = prepareCall(context, self);

finishBinding(call.entry, call.handle, self, call.selfClass, call.switchPoint);

try {
return (IRubyObject) call.handle.invokeExact(context, self, arg0, arg1, arg2);
} catch (Throwable t) {
throwException(t);
return null; // not reached
}
}

private Call prepareCall(ThreadContext context, IRubyObject self) throws Throwable {
return prepareCall(context, RubyBasicObject.NEVER, self);
}

private Call prepareCall(ThreadContext context, IRubyObject caller, IRubyObject self) throws Throwable {
RubyClass selfClass = pollAndGetClass(context, self);
SwitchPoint switchPoint = (SwitchPoint) selfClass.getInvalidator().getData();
String methodName = this.methodName;
CacheEntry entry = selfClass.searchWithCache(methodName);
MethodHandle mh;
boolean passSymbol = false;

if (methodMissing(entry)) {
if (methodMissing(entry, caller)) {
entry = methodMissingEntry(context, selfClass, methodName, entry);
// only pass symbol below if we be calling a user-defined method_missing (default ones do it for us)
passSymbol = !(entry.method instanceof RubyKernel.MethodMissingMethod ||
Expand All @@ -675,11 +917,11 @@ public IRubyObject invoke(ThreadContext context, IRubyObject self, IRubyObject[]
mh = getHandle(self, entry);
}

finishBinding(entry, mh, self, selfClass, switchPoint);

return performIndirectCall(context, self, args, block, methodName, passSymbol, entry);
return new Call(entry, selfClass, mh, switchPoint, passSymbol);
}

private record Call(CacheEntry entry, RubyClass selfClass, MethodHandle handle, SwitchPoint switchPoint, boolean passSymbol) {}

private CacheEntry methodMissingEntry(ThreadContext context, RubyClass selfClass, String methodName, CacheEntry entry) {
// Test thresholds so we don't do this forever (#4596)
if (testThresholds(selfClass) == CacheAction.FAIL) {
Expand Down Expand Up @@ -711,31 +953,6 @@ private void finishBinding(CacheEntry entry, MethodHandle mh, IRubyObject self,
updateInvocationTarget(mh, self, selfClass, entry.method, switchPoint);
}

private IRubyObject performIndirectCall(ThreadContext context, IRubyObject self, IRubyObject[] args, Block block, String methodName, boolean passSymbol, CacheEntry entry) {
RubyModule sourceModule = entry.sourceModule;
DynamicMethod method = entry.method;

IRRuntimeHelpers.setCallInfo(context, flags);

if (literalClosure) {
try {
if (passSymbol) {
return method.call(context, self, sourceModule, "method_missing", Helpers.arrayOf(context.runtime.newSymbol(methodName), args), block);
} else {
return method.call(context, self, sourceModule, methodName, args, block);
}
} finally {
block.escape();
}
}

if (passSymbol) {
return method.call(context, self, sourceModule, methodName, Helpers.arrayOf(context.runtime.newSymbol(methodName), args), block);
} else {
return method.call(context, self, sourceModule, methodName, args, block);
}
}

private static final MethodHandle ESCAPE_BLOCK = Binder.from(void.class, Block.class).invokeVirtualQuiet(LOOKUP, "escape");
private static final Map<Signature, MethodHandle> BLOCK_ESCAPES = Collections.synchronizedMap(new HashMap<Signature, MethodHandle>());

Expand Down Expand Up @@ -977,21 +1194,14 @@ public Binder prepareBinder(boolean varargs) {

if (varargs || arity > 3) {
// we know we want to call varargs path always, so prepare args[] here
if (arity == -1) {
// do nothing, already have IRubyObject[] in args
} else if (arity == 0) {
binder = binder.insert(argOffset, "args", IRubyObject.NULL_ARRAY);
if (arity == -1 || arity == 0) {
// do nothing, already have IRubyObject[] or nothing in args
} else {
binder = binder
.collect("args", "arg[0-9]+", Helpers.constructObjectArrayHandle(arity));
}
}

// add block if needed
if (signature.lastArgType() != Block.class) {
binder = binder.append("block", Block.NULL_BLOCK);
}

// bind to site
binder = binder.insert(0, "site", this);

Expand Down Expand Up @@ -1396,6 +1606,8 @@ public void setInitialTarget(MethodHandle target) {
}

public boolean methodMissing(CacheEntry entry, IRubyObject caller) {
if (caller == RubyBasicObject.NEVER) return methodMissing(entry);

DynamicMethod method = entry.method;

return method.isUndefined() || (!methodName.equals("method_missing") && !method.isCallableFrom(caller, callType));
Expand Down

0 comments on commit b9f5b2a

Please sign in to comment.