Skip to content

Commit a13afa5

Browse files
committed
GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining
1 parent b5dffe7 commit a13afa5

6 files changed

Lines changed: 771 additions & 37 deletions

File tree

src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
2323
import org.codehaus.groovy.runtime.memoize.MemoizeCache;
2424

25+
import java.io.Serial;
2526
import java.lang.invoke.MethodHandle;
2627
import java.lang.invoke.MethodHandles;
2728
import java.lang.invoke.MethodType;
@@ -52,7 +53,7 @@ public class CacheableCallSite extends MutableCallSite {
5253
private MethodHandle fallbackTarget;
5354
private final Map<String, SoftReference<MethodHandleWrapper>> lruCache =
5455
new LinkedHashMap<String, SoftReference<MethodHandleWrapper>>(INITIAL_CAPACITY, LOAD_FACTOR, true) {
55-
private static final long serialVersionUID = 7785958879964294463L;
56+
@Serial private static final long serialVersionUID = 7785958879964294463L;
5657

5758
@Override
5859
protected boolean removeEldestEntry(Map.Entry eldest) {

src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import groovy.lang.GroovySystem;
2222
import org.apache.groovy.util.SystemUtil;
2323
import org.codehaus.groovy.GroovyBugError;
24-
import org.codehaus.groovy.reflection.ClassInfo;
2524
import org.codehaus.groovy.runtime.GeneratedClosure;
2625

2726
import java.lang.invoke.CallSite;
@@ -31,6 +30,7 @@
3130
import java.lang.invoke.MethodType;
3231
import java.lang.invoke.MutableCallSite;
3332
import java.lang.invoke.SwitchPoint;
33+
import java.lang.reflect.Modifier;
3434
import java.util.Map;
3535
import java.util.function.Function;
3636
import java.util.logging.Level;
@@ -320,48 +320,49 @@ public static Object fromCache(CacheableCallSite callSite, Class<?> sender, Stri
320320
*/
321321
private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class<?> sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable {
322322
FallbackSupplier fallbackSupplier = new FallbackSupplier(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments);
323-
324-
MethodHandleWrapper mhw;
325-
if (bypassCache(spreadCall, arguments)) {
326-
mhw = NULL_METHOD_HANDLE_WRAPPER;
327-
} else {
328-
Object receiver = arguments[0];
329-
String receiverClassName = receiver != null ? receiver.getClass().getName() : NULL_OBJECT_CLASS_NAME;
330-
331-
mhw = callSite.getAndPut(receiverClassName, (theName) -> {
332-
MethodHandleWrapper fallback = fallbackSupplier.get();
333-
if (fallback.isCanSetTarget()) return fallback;
334-
return NULL_METHOD_HANDLE_WRAPPER;
335-
});
336-
}
323+
Object receiver = arguments[0];
324+
String receiverClassName = receiverCacheKey(receiver);
325+
MethodHandleWrapper mhw = callSite.getAndPut(receiverClassName, (theName) -> {
326+
MethodHandleWrapper fallback = fallbackSupplier.get();
327+
if (fallback.isCanSetTarget()) return fallback;
328+
return NULL_METHOD_HANDLE_WRAPPER;
329+
});
337330

338331
if (mhw == NULL_METHOD_HANDLE_WRAPPER) {
332+
// The PIC stores a sentinel to remember "do not relink this receiver shape";
333+
// execution still needs a real handle for the current invocation.
339334
mhw = fallbackSupplier.get();
340335
}
341336

342-
if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle()) && (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD)) {
343-
if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) {
344-
if (callSite.getTarget() != callSite.getDefaultTarget()) {
345-
// reset the call site target to default forever to avoid JIT deoptimization storm further
346-
callSite.setTarget(callSite.getDefaultTarget());
337+
if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle())) {
338+
// GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining.
339+
if (callSite.type().parameterType(0) == Class.class) {
340+
var method = mhw.getMethod();
341+
if (method != null && Modifier.isStatic(method.getModifiers())) {
342+
callSite.setTarget(mhw.getTargetMethodHandle());
347343
}
348-
} else {
349-
callSite.setTarget(mhw.getTargetMethodHandle());
350-
if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation");
351344
}
352345

353-
mhw.resetLatestHitCount();
346+
if (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD) {
347+
if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) {
348+
if (callSite.getTarget() != callSite.getDefaultTarget()) {
349+
// reset the call site target to default forever to avoid JIT deoptimization storm further
350+
callSite.setTarget(callSite.getDefaultTarget());
351+
}
352+
} else {
353+
if (callSite.getTarget() != mhw.getTargetMethodHandle()) {
354+
callSite.setTarget(mhw.getTargetMethodHandle());
355+
if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation");
356+
}
357+
}
358+
359+
mhw.resetLatestHitCount();
360+
}
354361
}
355362

356363
return mhw.getCachedMethodHandle();
357364
}
358365

359-
private static boolean bypassCache(Boolean spreadCall, Object[] arguments) {
360-
if (spreadCall) return true;
361-
Object receiver = arguments[0];
362-
return receiver != null && ClassInfo.getClassInfo(receiver.getClass()).hasPerInstanceMetaClasses();
363-
}
364-
365366
/**
366367
* Core method for indy method selection using runtime types.
367368
* @deprecated Use the new bootHandle-based approach instead.
@@ -390,20 +391,34 @@ private static MethodHandle selectMethodHandle(CacheableCallSite callSite, Class
390391
// correct the stale methodHandle in the inline cache of callsite
391392
// it is important but impacts the performance somehow when cache misses frequently
392393
Object receiver = arguments[0];
393-
String key = receiver != null ? receiver.getClass().getName() : NULL_OBJECT_CLASS_NAME;
394-
callSite.put(key, mhw);
394+
395+
// Avoid PIC pollution: don't write back uncached wrappers, e.g. for instance-level metaClass dispatches.
396+
callSite.put(receiverCacheKey(receiver), mhw.isCanSetTarget() ? mhw : NULL_METHOD_HANDLE_WRAPPER);
395397
}
396398

397399
return mhw.getCachedMethodHandle();
398400
}
399401

402+
/**
403+
* Computes the PIC cache key for the given receiver.
404+
* Different {@code Class} objects (e.g. {@code A} vs {@code B}) share the same runtime class
405+
* ({@code java.lang.Class}) but dispatch to different methods. Including the represented class
406+
* name avoids PIC cache collisions for static-method call sites.
407+
*/
408+
private static String receiverCacheKey(Object receiver) {
409+
if (receiver == null) return NULL_OBJECT_CLASS_NAME;
410+
if (receiver instanceof Class<?> c) return "java.lang.Class:" + c.getName();
411+
return receiver.getClass().getName();
412+
}
413+
400414
private static MethodHandleWrapper fallback(CacheableCallSite callSite, Class<?> sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) {
401415
Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments);
402416
selector.setCallSiteTarget();
403417

404418
return new MethodHandleWrapper(
405419
selector.handle.asSpreader(Object[].class, arguments.length).asType(MethodType.methodType(Object.class, Object[].class)),
406420
selector.handle,
421+
selector.method,
407422
selector.cache
408423
);
409424
}

src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
package org.codehaus.groovy.vmplugin.v8;
2020

21+
import groovy.lang.MetaMethod;
22+
2123
import java.lang.invoke.MethodHandle;
2224
import java.util.concurrent.atomic.AtomicLong;
2325

@@ -29,12 +31,14 @@
2931
class MethodHandleWrapper {
3032
private final MethodHandle cachedMethodHandle;
3133
private final MethodHandle targetMethodHandle;
34+
private final MetaMethod method;
3235
private final boolean canSetTarget;
3336
private final AtomicLong latestHitCount = new AtomicLong(0);
3437

35-
public MethodHandleWrapper(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, boolean canSetTarget) {
38+
public MethodHandleWrapper(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, MetaMethod method, boolean canSetTarget) {
3639
this.cachedMethodHandle = cachedMethodHandle;
3740
this.targetMethodHandle = targetMethodHandle;
41+
this.method = method;
3842
this.canSetTarget = canSetTarget;
3943
}
4044

@@ -46,6 +50,10 @@ public MethodHandle getTargetMethodHandle() {
4650
return targetMethodHandle;
4751
}
4852

53+
public MetaMethod getMethod() {
54+
return method;
55+
}
56+
4957
public boolean isCanSetTarget() {
5058
return canSetTarget;
5159
}
@@ -67,10 +75,10 @@ public static MethodHandleWrapper getNullMethodHandleWrapper() {
6775
}
6876

6977
private static class NullMethodHandleWrapper extends MethodHandleWrapper {
70-
public static final NullMethodHandleWrapper INSTANCE = new NullMethodHandleWrapper(null, null, false);
78+
public static final NullMethodHandleWrapper INSTANCE = new NullMethodHandleWrapper();
7179

72-
private NullMethodHandleWrapper(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, boolean canSetTarget) {
73-
super(cachedMethodHandle, targetMethodHandle, canSetTarget);
80+
private NullMethodHandleWrapper() {
81+
super(null, null, null, false);
7482
}
7583
}
7684
}

0 commit comments

Comments
 (0)