Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,19 @@ public MethodHandleWrapper getAndPut(String className, MemoizeCache.ValueProvide
lruCache.put(className, resultSoftReference);
}
}
final SoftReference<MethodHandleWrapper> mhwsr = latestHitMethodHandleWrapperSoftReference;
final MethodHandleWrapper methodHandleWrapper = null == mhwsr ? null : mhwsr.get();

if (methodHandleWrapper == result) {
final SoftReference<MethodHandleWrapper> latestHitReference = latestHitMethodHandleWrapperSoftReference;
if (latestHitReference == resultSoftReference) {
result.incrementLatestHitCount();
} else {
result.resetLatestHitCount();
if (null != methodHandleWrapper) methodHandleWrapper.resetLatestHitCount();
latestHitMethodHandleWrapperSoftReference = resultSoftReference;
final MethodHandleWrapper latestHitMethodHandleWrapper = null == latestHitReference ? null : latestHitReference.get();
if (latestHitMethodHandleWrapper == result) {
result.incrementLatestHitCount();
latestHitMethodHandleWrapperSoftReference = resultSoftReference;
} else {
result.resetLatestHitCount();
if (null != latestHitMethodHandleWrapper) latestHitMethodHandleWrapper.resetLatestHitCount();
latestHitMethodHandleWrapperSoftReference = resultSoftReference;
}
}

return result;
Expand Down
80 changes: 57 additions & 23 deletions src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import java.lang.invoke.MethodType;
import java.lang.invoke.MutableCallSite;
import java.lang.invoke.SwitchPoint;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.function.Function;
import java.util.logging.Level;
Expand Down Expand Up @@ -371,35 +370,69 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class<?>
mhw = fallbackSupplier.get();
}

if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle())) {
// GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining.
if (callSite.type().parameterType(0) == Class.class) {
var method = mhw.getMethod();
if (method != null && Modifier.isStatic(method.getModifiers())) {
callSite.setTarget(mhw.getTargetMethodHandle());
}
}

if (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD) {
if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) {
if (callSite.getTarget() != callSite.getDefaultTarget()) {
// reset the call site target to default forever to avoid JIT deoptimization storm further
callSite.setTarget(callSite.getDefaultTarget());
if (mhw.isCanSetTarget()) {
long latestHitCount = mhw.getLatestHitCount();
boolean optimizeTarget = latestHitCount > INDY_OPTIMIZE_THRESHOLD;
boolean setTargetEarly = shouldSetCallSiteTargetEarly(mhw, receiver, latestHitCount);

if (setTargetEarly || optimizeTarget) {
MethodHandle targetMethodHandle = mhw.getTargetMethodHandle();
MethodHandle currentTarget = callSite.getTarget();
if (currentTarget != targetMethodHandle) {
if (setTargetEarly) {
callSite.setTarget(targetMethodHandle);
currentTarget = targetMethodHandle;
}
} else {
if (callSite.getTarget() != mhw.getTargetMethodHandle()) {
callSite.setTarget(mhw.getTargetMethodHandle());
if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation");

if (optimizeTarget) {
if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) {
MethodHandle defaultTarget = callSite.getDefaultTarget();
if (currentTarget != defaultTarget) {
// reset the call site target to default forever to avoid JIT deoptimization storm further
callSite.setTarget(defaultTarget);
}
} else {
if (currentTarget != targetMethodHandle) {
callSite.setTarget(targetMethodHandle);
if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation");
}
}

mhw.resetLatestHitCount();
}
}

mhw.resetLatestHitCount();
}
}

return mhw.getCachedMethodHandle();
}

/**
* GROOVY-11935: install direct-looking targets early when the receiver shape is already
* specific enough to make earlier JIT inlining worthwhile.
*
* <p>Three cases trigger early relinking (in priority order):
* <ol>
* <li><b>Private method (static or instance)</b> — non-overridable by definition; the
* dispatch target is uniquely determined regardless of the call-site receiver type,
* so relinking is safe on the very first hit.</li>
* <li><b>Static call on a {@code Class} receiver</b> — the dispatch target
* is fully determined by the declared call-site type; relink on first hit.</li>
* <li><b>Final receiver type</b> — the JVM verifier guarantees that any non-null,
* non-{@code Class} object reaching a call site whose static parameter type is a
* {@code final} class is exactly that class (no subclass can exist). The runtime
* type therefore needs no separate equality check; one repeated hit is still
* required to avoid thrashing cold sites.</li>
* </ol>
*/
private static boolean shouldSetCallSiteTargetEarly(MethodHandleWrapper mhw, Object receiver, long latestHitCount) {
if (mhw.shouldSetCallSiteTargetImmediately()) return true;

return mhw.shouldSetCallSiteTargetOnRepeatedHit()
&& latestHitCount > 0
&& receiver != null;
}

/**
* Core method for indy method selection using runtime types.
* @deprecated Use the new bootHandle-based approach instead.
Expand Down Expand Up @@ -452,11 +485,12 @@ private static MethodHandleWrapper fallback(CacheableCallSite callSite, Class<?>
Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments);
selector.setCallSiteTarget();

return new MethodHandleWrapper(
return MethodHandleWrapper.create(
selector.handle.asSpreader(Object[].class, arguments.length).asType(MethodType.methodType(Object.class, Object[].class)),
selector.handle,
selector.method,
selector.cache
selector.cache,
callSite.type().parameterType(0)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import groovy.lang.MetaMethod;

import java.lang.invoke.MethodHandle;
import java.lang.reflect.Modifier;
import java.util.concurrent.atomic.AtomicLong;

/**
Expand All @@ -33,6 +34,7 @@ class MethodHandleWrapper {
private final MethodHandle targetMethodHandle;
private final MetaMethod method;
private final boolean canSetTarget;
private final CallSiteTargetRelinkPolicy callSiteTargetRelinkPolicy;
private final AtomicLong latestHitCount = new AtomicLong(0);

/**
Expand All @@ -44,10 +46,35 @@ class MethodHandleWrapper {
* @param canSetTarget whether the call site target may be updated to this handle
*/
public MethodHandleWrapper(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, MetaMethod method, boolean canSetTarget) {
this(cachedMethodHandle, targetMethodHandle, method, canSetTarget, CallSiteTargetRelinkPolicy.NEVER);
}

private MethodHandleWrapper(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, MetaMethod method, boolean canSetTarget, CallSiteTargetRelinkPolicy callSiteTargetRelinkPolicy) {
this.cachedMethodHandle = cachedMethodHandle;
this.targetMethodHandle = targetMethodHandle;
this.method = method;
this.canSetTarget = canSetTarget;
this.callSiteTargetRelinkPolicy = callSiteTargetRelinkPolicy;
}

/**
* Creates a wrapper and precomputes the early call-site relink policy for the supplied receiver type.
*
* @param cachedMethodHandle the cached invocation handle
* @param targetMethodHandle the relink target handle
* @param method the associated meta method
* @param canSetTarget whether the call site target may be updated to this handle
* @param receiverType the declared call-site receiver type
* @return the configured wrapper
*/
static MethodHandleWrapper create(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, MetaMethod method, boolean canSetTarget, Class<?> receiverType) {
return new MethodHandleWrapper(
cachedMethodHandle,
targetMethodHandle,
method,
canSetTarget,
resolveCallSiteTargetRelinkPolicy(method, receiverType)
);
}

/**
Expand Down Expand Up @@ -86,6 +113,24 @@ public boolean isCanSetTarget() {
return canSetTarget;
}

/**
* Indicates whether the call site can be relinked on the first cache hit.
*
* @return {@code true} when the target can be installed immediately
*/
boolean shouldSetCallSiteTargetImmediately() {
return callSiteTargetRelinkPolicy == CallSiteTargetRelinkPolicy.IMMEDIATE;
}

/**
* Indicates whether the call site can be relinked after observing a repeated exact-final receiver hit.
*
* @return {@code true} when a repeated hit may trigger relinking
*/
boolean shouldSetCallSiteTargetOnRepeatedHit() {
return callSiteTargetRelinkPolicy == CallSiteTargetRelinkPolicy.AFTER_REPEATED_HIT;
}

/**
* Increments the hit count for the latest inline-cache hit.
*
Expand Down Expand Up @@ -120,6 +165,29 @@ public static MethodHandleWrapper getNullMethodHandleWrapper() {
return NullMethodHandleWrapper.INSTANCE;
}

private static CallSiteTargetRelinkPolicy resolveCallSiteTargetRelinkPolicy(MetaMethod method, Class<?> receiverType) {
if (method == null) return CallSiteTargetRelinkPolicy.NEVER;

int modifiers = method.getModifiers();
if (Modifier.isPrivate(modifiers)) return CallSiteTargetRelinkPolicy.IMMEDIATE;
if (Modifier.isStatic(modifiers)) {
return receiverType == Class.class
? CallSiteTargetRelinkPolicy.IMMEDIATE
: CallSiteTargetRelinkPolicy.NEVER;
}
if (receiverType == Class.class) return CallSiteTargetRelinkPolicy.NEVER;

return Modifier.isFinal(receiverType.getModifiers())
? CallSiteTargetRelinkPolicy.AFTER_REPEATED_HIT
: CallSiteTargetRelinkPolicy.NEVER;
}

private enum CallSiteTargetRelinkPolicy {
NEVER,
IMMEDIATE,
AFTER_REPEATED_HIT
}

private static class NullMethodHandleWrapper extends MethodHandleWrapper {
/**
* Shared sentinel wrapper representing the absence of a reusable method handle.
Expand Down
Loading
Loading