You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/04/20 17:13:59 UTC
(groovy) branch GROOVY-11935 updated (42bd244a05 -> 70ba190e2d)
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a change to branch GROOVY-11935
in repository https://gitbox.apache.org/repos/asf/groovy.git
discard 42bd244a05 GROOVY-11935: Set invokedynamic call site target immediately for static method calls to enable earlier JIT inlining
new 70ba190e2d GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (42bd244a05)
\
N -- N -- N refs/heads/GROOVY-11935 (70ba190e2d)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java | 11 +++--------
.../groovy/vmplugin/v8/IndyInterfaceDeprecatedTest.groovy | 5 ++---
2 files changed, 5 insertions(+), 11 deletions(-)
(groovy) 01/01: GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining
Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch GROOVY-11935
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 70ba190e2db70a9ec949659c8800444f9275180d
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Apr 18 21:33:34 2026 +0900
GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining
---
.../groovy/vmplugin/v8/CacheableCallSite.java | 3 +-
.../codehaus/groovy/vmplugin/v8/IndyInterface.java | 76 ++--
.../groovy/vmplugin/v8/MethodHandleWrapper.java | 16 +-
.../vmplugin/v8/IndyInterfaceDeprecatedTest.groovy | 428 ++++++++++++++++++++-
.../groovy/bench/StaticMethodCallIndy.groovy | 124 ++++++
.../groovy/bench/StaticMethodCallIndyBench.java | 180 +++++++++
6 files changed, 770 insertions(+), 57 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java
index 73527d5227..53ac15ae63 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java
@@ -22,6 +22,7 @@ import org.apache.groovy.util.SystemUtil;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.memoize.MemoizeCache;
+import java.io.Serial;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
@@ -52,7 +53,7 @@ public class CacheableCallSite extends MutableCallSite {
private MethodHandle fallbackTarget;
private final Map<String, SoftReference<MethodHandleWrapper>> lruCache =
new LinkedHashMap<String, SoftReference<MethodHandleWrapper>>(INITIAL_CAPACITY, LOAD_FACTOR, true) {
- private static final long serialVersionUID = 7785958879964294463L;
+ @Serial private static final long serialVersionUID = 7785958879964294463L;
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
index 91682cc722..401c8307f0 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
@@ -21,7 +21,6 @@ package org.codehaus.groovy.vmplugin.v8;
import groovy.lang.GroovySystem;
import org.apache.groovy.util.SystemUtil;
import org.codehaus.groovy.GroovyBugError;
-import org.codehaus.groovy.reflection.ClassInfo;
import org.codehaus.groovy.runtime.GeneratedClosure;
import java.lang.invoke.CallSite;
@@ -31,6 +30,7 @@ import java.lang.invoke.MethodHandles;
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;
@@ -320,48 +320,46 @@ public class IndyInterface {
*/
private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class<?> sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable {
FallbackSupplier fallbackSupplier = new FallbackSupplier(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments);
-
- MethodHandleWrapper mhw;
- if (bypassCache(spreadCall, arguments)) {
- mhw = NULL_METHOD_HANDLE_WRAPPER;
- } else {
- Object receiver = arguments[0];
- String receiverClassName = receiver != null ? receiver.getClass().getName() : NULL_OBJECT_CLASS_NAME;
-
- mhw = callSite.getAndPut(receiverClassName, (theName) -> {
- MethodHandleWrapper fallback = fallbackSupplier.get();
- if (fallback.isCanSetTarget()) return fallback;
- return NULL_METHOD_HANDLE_WRAPPER;
- });
- }
+ Object receiver = arguments[0];
+ String receiverClassName = receiverCacheKey(receiver);
+ MethodHandleWrapper mhw = callSite.getAndPut(receiverClassName, (theName) -> {
+ MethodHandleWrapper fallback = fallbackSupplier.get();
+ if (fallback.isCanSetTarget()) return fallback;
+ return NULL_METHOD_HANDLE_WRAPPER;
+ });
if (mhw == NULL_METHOD_HANDLE_WRAPPER) {
+ // The PIC stores a sentinel to remember "do not relink this receiver shape";
+ // execution still needs a real handle for the current invocation.
mhw = fallbackSupplier.get();
}
- if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle()) && (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());
- }
- } else {
+ if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle())) {
+ // GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining.
+ if (Modifier.isFinal(callSite.type().parameterType(0).getModifiers())) {
callSite.setTarget(mhw.getTargetMethodHandle());
- if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation");
}
- mhw.resetLatestHitCount();
+ 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());
+ }
+ } else {
+ if (callSite.getTarget() != mhw.getTargetMethodHandle()) {
+ callSite.setTarget(mhw.getTargetMethodHandle());
+ if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation");
+ }
+ }
+
+ mhw.resetLatestHitCount();
+ }
}
return mhw.getCachedMethodHandle();
}
- private static boolean bypassCache(Boolean spreadCall, Object[] arguments) {
- if (spreadCall) return true;
- Object receiver = arguments[0];
- return receiver != null && ClassInfo.getClassInfo(receiver.getClass()).hasPerInstanceMetaClasses();
- }
-
/**
* Core method for indy method selection using runtime types.
* @deprecated Use the new bootHandle-based approach instead.
@@ -390,13 +388,26 @@ public class IndyInterface {
// correct the stale methodHandle in the inline cache of callsite
// it is important but impacts the performance somehow when cache misses frequently
Object receiver = arguments[0];
- String key = receiver != null ? receiver.getClass().getName() : NULL_OBJECT_CLASS_NAME;
- callSite.put(key, mhw);
+
+ // Avoid PIC pollution: don't write back uncached wrappers, e.g. for instance-level metaClass dispatches.
+ callSite.put(receiverCacheKey(receiver), mhw.isCanSetTarget() ? mhw : NULL_METHOD_HANDLE_WRAPPER);
}
return mhw.getCachedMethodHandle();
}
+ /**
+ * Computes the PIC cache key for the given receiver.
+ * Different {@code Class} objects (e.g. {@code A} vs {@code B}) share the same runtime class
+ * ({@code java.lang.Class}) but dispatch to different methods. Including the represented class
+ * name avoids PIC cache collisions for static-method call sites.
+ */
+ private static String receiverCacheKey(Object receiver) {
+ if (receiver == null) return NULL_OBJECT_CLASS_NAME;
+ if (receiver instanceof Class<?> c) return "java.lang.Class:" + c.getName();
+ return receiver.getClass().getName();
+ }
+
private static MethodHandleWrapper fallback(CacheableCallSite callSite, Class<?> sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) {
Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments);
selector.setCallSiteTarget();
@@ -404,6 +415,7 @@ public class IndyInterface {
return new MethodHandleWrapper(
selector.handle.asSpreader(Object[].class, arguments.length).asType(MethodType.methodType(Object.class, Object[].class)),
selector.handle,
+ selector.method,
selector.cache
);
}
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java
index b858464352..765c3d74ed 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java
@@ -18,6 +18,8 @@
*/
package org.codehaus.groovy.vmplugin.v8;
+import groovy.lang.MetaMethod;
+
import java.lang.invoke.MethodHandle;
import java.util.concurrent.atomic.AtomicLong;
@@ -29,12 +31,14 @@ import java.util.concurrent.atomic.AtomicLong;
class MethodHandleWrapper {
private final MethodHandle cachedMethodHandle;
private final MethodHandle targetMethodHandle;
+ private final MetaMethod method;
private final boolean canSetTarget;
private final AtomicLong latestHitCount = new AtomicLong(0);
- public MethodHandleWrapper(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, boolean canSetTarget) {
+ public MethodHandleWrapper(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, MetaMethod method, boolean canSetTarget) {
this.cachedMethodHandle = cachedMethodHandle;
this.targetMethodHandle = targetMethodHandle;
+ this.method = method;
this.canSetTarget = canSetTarget;
}
@@ -46,6 +50,10 @@ class MethodHandleWrapper {
return targetMethodHandle;
}
+ public MetaMethod getMethod() {
+ return method;
+ }
+
public boolean isCanSetTarget() {
return canSetTarget;
}
@@ -67,10 +75,10 @@ class MethodHandleWrapper {
}
private static class NullMethodHandleWrapper extends MethodHandleWrapper {
- public static final NullMethodHandleWrapper INSTANCE = new NullMethodHandleWrapper(null, null, false);
+ public static final NullMethodHandleWrapper INSTANCE = new NullMethodHandleWrapper();
- private NullMethodHandleWrapper(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, boolean canSetTarget) {
- super(cachedMethodHandle, targetMethodHandle, canSetTarget);
+ private NullMethodHandleWrapper() {
+ super(null, null, null, false);
}
}
}
diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceDeprecatedTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceDeprecatedTest.groovy
index d7a50dec72..586b6e22d3 100644
--- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceDeprecatedTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceDeprecatedTest.groovy
@@ -18,12 +18,20 @@
*/
package org.codehaus.groovy.vmplugin.v8
+
+import org.codehaus.groovy.reflection.CachedMethod
+import org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl
import org.junit.jupiter.api.Test
+import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
+import java.lang.reflect.Field
+import java.lang.reflect.Method
import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertNotSame
+import static org.junit.jupiter.api.Assertions.assertSame
final class IndyInterfaceDeprecatedTest {
@@ -31,19 +39,33 @@ final class IndyInterfaceDeprecatedTest {
return "foo-result"
}
+ private static String staticFoo() {
+ return "static-foo-result"
+ }
+
+ private static String staticEcho(String value) {
+ return "static-echo-$value"
+ }
+
+ private static final class PerInstanceMetaClassStaticTarget {
+ private static String ping() {
+ return 'per-instance-static-result'
+ }
+ }
+
+ // Helper classes for: def foo(receiver){receiver.bar()}
+ private static final class ClassA {
+ private static String bar() { return 'bar-from-A' }
+ }
+
+ private static final class ClassB {
+ private static String bar() { return 'bar-from-B' }
+ }
+
@Test
void testSelectMethodDeprecatedInstanceFoo() {
- // Prepare call site type: (IndyInterfaceDeprecatedTest) -> Object
- MethodHandles.Lookup lookup = MethodHandles.lookup()
MethodType type = MethodType.methodType(Object, IndyInterfaceDeprecatedTest)
- CacheableCallSite callSite = new CacheableCallSite(type, lookup)
-
- // Provide non-null default/fallback targets (needed for guards in Selector)
- def dummyTarget = MethodHandles.dropArguments(
- MethodHandles.constant(Object, null), 0, type.parameterArray()
- )
- callSite.defaultTarget = dummyTarget
- callSite.fallbackTarget = dummyTarget
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
// Prepare invocation arguments
def receiver = new IndyInterfaceDeprecatedTest()
@@ -69,17 +91,8 @@ final class IndyInterfaceDeprecatedTest {
@Test
void testFromCacheDeprecatedInstanceFoo() {
- // Prepare call site type: (IndyInterfaceDeprecatedTest) -> Object
- MethodHandles.Lookup lookup = MethodHandles.lookup()
MethodType type = MethodType.methodType(Object, IndyInterfaceDeprecatedTest)
- CacheableCallSite callSite = new CacheableCallSite(type, lookup)
-
- // Provide non-null default/fallback targets (needed for guards in Selector)
- def dummyTarget = MethodHandles.dropArguments(
- MethodHandles.constant(Object, null), 0, type.parameterArray()
- )
- callSite.defaultTarget = dummyTarget
- callSite.fallbackTarget = dummyTarget
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
// Prepare invocation arguments
def receiver = new IndyInterfaceDeprecatedTest()
@@ -102,4 +115,379 @@ final class IndyInterfaceDeprecatedTest {
// Verify the local method foo was actually called
assertEquals(receiver.foo(), result)
}
+
+ @Test
+ void testFromCacheDeprecatedStaticFooRelinksTargetImmediately() {
+ MethodType type = MethodType.methodType(Object, Class)
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+
+ Object[] args = [IndyInterfaceDeprecatedTest] as Object[]
+ int callID = IndyInterface.CallType.METHOD.getOrderNumber()
+
+ Object result = IndyInterface.fromCache(
+ callSite,
+ IndyInterfaceDeprecatedTest,
+ 'staticFoo',
+ callID,
+ Boolean.FALSE,
+ Boolean.FALSE,
+ Boolean.FALSE,
+ 1,
+ args
+ )
+
+ assertEquals(staticFoo(), result)
+ assertNotSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleDeprecatedStaticEchoKeepsDefaultTargetWhenCachingDisabled() {
+ MethodType type = MethodType.methodType(Object, Class, Object[])
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+
+ Object[] args = [IndyInterfaceDeprecatedTest, ['bar'] as Object[]] as Object[]
+ int callID = IndyInterface.CallType.METHOD.getOrderNumber()
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite,
+ IndyInterfaceDeprecatedTest,
+ 'staticEcho',
+ callID,
+ Boolean.FALSE,
+ Boolean.FALSE,
+ Boolean.TRUE,
+ 1,
+ args
+ )
+
+ assertEquals(staticEcho('bar'), methodHandle.invokeWithArguments([args] as Object[]))
+ assertSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandlePromotesCachedInstanceTargetAfterOptimizeThreshold() {
+ MethodType type = MethodType.methodType(Object, IndyInterfaceDeprecatedTest)
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+ def receiver = new IndyInterfaceDeprecatedTest()
+ Object[] args = [receiver] as Object[]
+ MethodHandleWrapper wrapper = newCachedWrapper(
+ type, 'cached-instance-result', 'optimized-target-result',
+ CachedMethod.find(IndyInterfaceDeprecatedTest.getDeclaredMethod('foo')), true
+ )
+
+ cacheWrapper(callSite, receiver, wrapper)
+ primeLatestHitCount(callSite, receiver, wrapper, readIndyLong('INDY_OPTIMIZE_THRESHOLD'))
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceDeprecatedTest, 'foo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(wrapper.cachedMethodHandle, methodHandle)
+ assertSame(wrapper.targetMethodHandle, callSite.target)
+ assertEquals(0L, wrapper.latestHitCount)
+ }
+
+ @Test
+ void testFromCacheHandleResetsToDefaultTargetAfterFallbackCutoff() {
+ MethodType type = MethodType.methodType(Object, IndyInterfaceDeprecatedTest)
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+ def receiver = new IndyInterfaceDeprecatedTest()
+ Object[] args = [receiver] as Object[]
+ MethodHandleWrapper wrapper = newCachedWrapper(
+ type, 'cached-instance-result', 'optimized-target-result',
+ CachedMethod.find(IndyInterfaceDeprecatedTest.getDeclaredMethod('foo')), true
+ )
+
+ cacheWrapper(callSite, receiver, wrapper)
+ primeLatestHitCount(callSite, receiver, wrapper, readIndyLong('INDY_OPTIMIZE_THRESHOLD'))
+ callSite.fallbackRound.set(readIndyLong('INDY_FALLBACK_CUTOFF') + 1L)
+ callSite.target = targetHandle(type, 'non-default-target')
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceDeprecatedTest, 'foo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(wrapper.cachedMethodHandle, methodHandle)
+ assertSame(callSite.defaultTarget, callSite.target)
+ assertEquals(0L, wrapper.latestHitCount)
+ }
+
+ @Test
+ void testFromCacheHandleLeavesDefaultTargetAfterFallbackCutoffWhenAlreadyDefault() {
+ MethodType type = MethodType.methodType(Object, IndyInterfaceDeprecatedTest)
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+ def receiver = new IndyInterfaceDeprecatedTest()
+ Object[] args = [receiver] as Object[]
+ MethodHandleWrapper wrapper = newCachedWrapper(
+ type, 'cached-instance-result', 'optimized-target-result',
+ CachedMethod.find(IndyInterfaceDeprecatedTest.getDeclaredMethod('foo')), true
+ )
+
+ cacheWrapper(callSite, receiver, wrapper)
+ primeLatestHitCount(callSite, receiver, wrapper, readIndyLong('INDY_OPTIMIZE_THRESHOLD'))
+ callSite.fallbackRound.set(readIndyLong('INDY_FALLBACK_CUTOFF') + 1L)
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceDeprecatedTest, 'foo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(wrapper.cachedMethodHandle, methodHandle)
+ assertSame(callSite.defaultTarget, callSite.target)
+ assertEquals(0L, wrapper.latestHitCount)
+ }
+
+ @Test
+ void testFromCacheHandleRelinksClassReceiverEvenWithoutStaticMetaMethod() {
+ MethodType type = MethodType.methodType(Object, Class)
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+ Object[] args = [IndyInterfaceDeprecatedTest] as Object[]
+ MethodHandleWrapper wrapper = newCachedWrapper(type, 'cached-class-result', 'class-target-result', null, true)
+
+ cacheWrapper(callSite, IndyInterfaceDeprecatedTest, wrapper)
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceDeprecatedTest, 'staticFoo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(wrapper.cachedMethodHandle, methodHandle)
+ assertSame(wrapper.targetMethodHandle, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleSkipsTargetChangesWhenCachedWrapperCannotSetTarget() {
+ MethodType type = MethodType.methodType(Object, IndyInterfaceDeprecatedTest)
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+ def receiver = new IndyInterfaceDeprecatedTest()
+ Object[] args = [receiver] as Object[]
+ MethodHandleWrapper wrapper = newCachedWrapper(type, 'uncacheable-result', 'ignored-target', null, false)
+
+ cacheWrapper(callSite, receiver, wrapper)
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceDeprecatedTest, 'foo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(wrapper.cachedMethodHandle, methodHandle)
+ assertSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleCachesSafeNavigationForNullReceiver() {
+ MethodType type = MethodType.methodType(Object, Object)
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+ Object[] args = [null] as Object[]
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceDeprecatedTest, 'foo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.TRUE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+
+ assertEquals(null, methodHandle.invokeWithArguments([args] as Object[]))
+ assertSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleUsesFallbackWhenClassReceiverCannotBeCached() {
+ MethodType type = MethodType.methodType(Object, Class)
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+ Object[] args = [PerInstanceMetaClassStaticTarget] as Object[]
+ MetaClassRegistryImpl registry = (MetaClassRegistryImpl) GroovySystem.metaClassRegistry
+ ExpandoMetaClass emc = new ExpandoMetaClass(PerInstanceMetaClassStaticTarget, false, true)
+ emc.initialize()
+
+ registry.setMetaClass((Object) PerInstanceMetaClassStaticTarget, emc)
+ try {
+ 2.times {
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, PerInstanceMetaClassStaticTarget, 'ping',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+
+ assertEquals(PerInstanceMetaClassStaticTarget.ping(), methodHandle.invokeWithArguments([args] as Object[]))
+ }
+ assertSame(callSite.defaultTarget, callSite.target)
+ } finally {
+ registry.setMetaClass((Object) PerInstanceMetaClassStaticTarget, (MetaClass) null)
+ }
+ }
+
+ @Test
+ void testFromCacheHandleLeavesAlreadyLinkedCachedTargetUntouched() {
+ MethodType type = MethodType.methodType(Object, IndyInterfaceDeprecatedTest)
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+ def receiver = new IndyInterfaceDeprecatedTest()
+ Object[] args = [receiver] as Object[]
+ MethodHandleWrapper wrapper = newCachedWrapper(
+ type, 'already-linked-result', 'linked-target-result',
+ CachedMethod.find(IndyInterfaceDeprecatedTest.getDeclaredMethod('foo')), true
+ )
+
+ cacheWrapper(callSite, receiver, wrapper)
+ callSite.target = wrapper.targetMethodHandle
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceDeprecatedTest, 'foo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(wrapper.cachedMethodHandle, methodHandle)
+ assertSame(wrapper.targetMethodHandle, callSite.target)
+ }
+
+ /**
+ * def foo(receiver){receiver.bar()} / foo(A) / foo(B)
+ * When the call site's static parameter type is Object (not Class), the early
+ * setTarget optimization must NOT fire — even if the runtime receiver is a Class.
+ */
+ @Test
+ void testFromCacheHandleDoesNotRelinkWhenCallSiteParamIsObjectEvenIfReceiverIsClass() {
+ // Simulate a call site typed as (Object)->Object (dynamic dispatch)
+ MethodType type = MethodType.methodType(Object, Object)
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+
+ Object[] args = [ClassA] as Object[]
+ int callID = IndyInterface.CallType.METHOD.getOrderNumber()
+
+ // Use a cacheable wrapper with a known static method
+ MethodHandleWrapper wrapper = newCachedWrapper(
+ type, 'class-a-result', 'class-a-target',
+ CachedMethod.find(ClassA.getDeclaredMethod('bar')), true
+ )
+ cacheWrapper(callSite, ClassA, wrapper)
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, ClassA, 'bar', callID,
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(wrapper.cachedMethodHandle, methodHandle)
+ // Must NOT have set target — call site param type is Object, not Class
+ assertSame(callSite.defaultTarget, callSite.target)
+ }
+
+ /**
+ * Verifies that different Class receivers (A vs B) get distinct PIC entries,
+ * avoiding the cache collision where both would previously map to "java.lang.Class".
+ */
+ @Test
+ void testFromCacheHandleDistinguishesDifferentClassReceivers() {
+ MethodType type = MethodType.methodType(Object, Class)
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+ int callID = IndyInterface.CallType.METHOD.getOrderNumber()
+
+ // Call with ClassA
+ Object[] argsA = [ClassA] as Object[]
+ MethodHandle handleA = invokeFromCacheHandle(
+ callSite, ClassA, 'bar', callID,
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsA
+ )
+ assertEquals(ClassA.bar(), handleA.invokeWithArguments([argsA] as Object[]))
+
+ // Call with ClassB — must resolve to B.bar(), not A.bar()
+ Object[] argsB = [ClassB] as Object[]
+ MethodHandle handleB = invokeFromCacheHandle(
+ callSite, ClassB, 'bar', callID,
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsB
+ )
+ assertEquals(ClassB.bar(), handleB.invokeWithArguments([argsB] as Object[]))
+ }
+
+ /**
+ * When the call site type has Class as first param AND the method is static,
+ * the early setTarget optimization should still fire correctly.
+ */
+ @Test
+ void testFromCacheHandleRelinksImmediatelyForClassTypedCallSiteWithStaticMethod() {
+ MethodType type = MethodType.methodType(Object, Class)
+ CacheableCallSite callSite = newDeprecatedCallSite(type)
+
+ MethodHandleWrapper wrapper = newCachedWrapper(
+ type, 'cached-static-result', 'static-target-result',
+ CachedMethod.find(ClassA.getDeclaredMethod('bar')), true
+ )
+ cacheWrapper(callSite, ClassA, wrapper)
+
+ Object[] args = [ClassA] as Object[]
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, ClassA, 'bar',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(wrapper.cachedMethodHandle, methodHandle)
+ // Early setTarget should have fired (Class-typed call site + static method)
+ assertSame(wrapper.targetMethodHandle, callSite.target)
+ }
+
+ private static CacheableCallSite newDeprecatedCallSite(MethodType type) {
+ CacheableCallSite callSite = new CacheableCallSite(type, MethodHandles.lookup())
+ MethodHandle dummyTarget = targetHandle(type, null)
+ callSite.target = dummyTarget
+ callSite.defaultTarget = dummyTarget
+ callSite.fallbackTarget = dummyTarget
+ return callSite
+ }
+
+ private static MethodHandleWrapper newCachedWrapper(MethodType type, Object cachedValue, Object targetValue, MetaMethod method, boolean canSetTarget) {
+ new MethodHandleWrapper(cachedHandle(cachedValue), targetHandle(type, targetValue), method, canSetTarget)
+ }
+
+ private static MethodHandle cachedHandle(Object value) {
+ MethodHandles.dropArguments(MethodHandles.constant(Object, value), 0, Object[])
+ }
+
+ private static MethodHandle targetHandle(MethodType type, Object value) {
+ MethodHandles.dropArguments(MethodHandles.constant(Object, value), 0, type.parameterArray())
+ }
+
+ private static void cacheWrapper(CacheableCallSite callSite, Object receiver, MethodHandleWrapper wrapper) {
+ callSite.put(receiverClassName(receiver), wrapper)
+ }
+
+ private static void primeLatestHitCount(CacheableCallSite callSite, Object receiver, MethodHandleWrapper wrapper, long value) {
+ assertSame(wrapper, callSite.getAndPut(receiverClassName(receiver), { wrapper }))
+ latestHitCountField().get(wrapper).set(value)
+ }
+
+ private static String receiverClassName(Object receiver) {
+ if (receiver == null) return 'org.codehaus.groovy.runtime.NullObject'
+ if (receiver instanceof Class) return 'java.lang.Class:' + ((Class<?>) receiver).getName()
+ return receiver.getClass().name
+ }
+
+ private static long readIndyLong(String fieldName) {
+ Field field = IndyInterface.getDeclaredField(fieldName)
+ field.accessible = true
+ field.getLong(null)
+ }
+
+ private static Field latestHitCountField() {
+ Field field = MethodHandleWrapper.getDeclaredField('latestHitCount')
+ field.accessible = true
+ field
+ }
+
+ private static MethodHandle invokeFromCacheHandle(CacheableCallSite callSite, Class<?> sender, String methodName,
+ int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) {
+ Method method = IndyInterface.getDeclaredMethod('fromCacheHandle',
+ CacheableCallSite, Class, String, Integer.TYPE, Boolean, Boolean, Boolean, Object, Object[]
+ )
+ method.accessible = true
+ return (MethodHandle) method.invoke(null,
+ callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments
+ )
+ }
}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/StaticMethodCallIndy.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/StaticMethodCallIndy.groovy
new file mode 100644
index 0000000000..5024e1e853
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/StaticMethodCallIndy.groovy
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.groovy.bench
+
+import groovy.transform.CompileStatic
+
+/**
+ * Helper class for {@link StaticMethodCallIndyBench}.
+ * <p>
+ * Dynamic methods emit {@code invokedynamic}
+ * call sites whose targets are resolved through {@code IndyInterface}.
+ * Static method calls benefit from the early {@code setTarget} optimization
+ * because the receiver is always a {@code Class} object, allowing the JIT
+ * compiler to inline the target sooner.
+ */
+class StaticMethodCallIndy {
+
+ // ---------- Dynamic Groovy — invokedynamic call sites ----------
+
+ static int staticAdd(int a, int b) {
+ return a + b
+ }
+
+ static int staticSum(int n) {
+ int sum = 0
+ for (int i = 0; i < n; i++) {
+ sum = staticAdd(sum, i)
+ }
+ return sum
+ }
+
+ static int staticFib(int n) {
+ if (n < 2) return n
+ return staticFib(n - 1) + staticFib(n - 2)
+ }
+
+ static int staticSquare(int x) { return x * x }
+ static int staticIncrement(int x) { return x + 1 }
+ static int staticDouble(int x) { return x * 2 }
+
+ static int staticChain(int x) {
+ return staticDouble(staticIncrement(staticSquare(x)))
+ }
+
+ // ---------- Instance methods — no early setTarget ----------
+
+ int instanceAdd(int a, int b) {
+ return a + b
+ }
+
+ int instanceSum(int n) {
+ int sum = 0
+ for (int i = 0; i < n; i++) {
+ sum = instanceAdd(sum, i)
+ }
+ return sum
+ }
+
+ int instanceFib(int n) {
+ if (n < 2) return n
+ return instanceFib(n - 1) + instanceFib(n - 2)
+ }
+
+ int instanceSquare(int x) { return x * x }
+ int instanceIncrement(int x) { return x + 1 }
+ int instanceDouble(int x) { return x * 2 }
+
+ int instanceChain(int x) {
+ return instanceDouble(instanceIncrement(instanceSquare(x)))
+ }
+
+ // ---------- @CompileStatic — invokestatic, no invokedynamic ----------
+
+ @CompileStatic
+ static int staticAddCS(int a, int b) {
+ return a + b
+ }
+
+ @CompileStatic
+ static int staticSumCS(int n) {
+ int sum = 0
+ for (int i = 0; i < n; i++) {
+ sum = staticAddCS(sum, i)
+ }
+ return sum
+ }
+
+ @CompileStatic
+ static int staticFibCS(int n) {
+ if (n < 2) return n
+ return staticFibCS(n - 1) + staticFibCS(n - 2)
+ }
+
+ @CompileStatic
+ static int staticSquareCS(int x) { return x * x }
+
+ @CompileStatic
+ static int staticIncrementCS(int x) { return x + 1 }
+
+ @CompileStatic
+ static int staticDoubleCS(int x) { return x * 2 }
+
+ @CompileStatic
+ static int staticChainCS(int x) {
+ return staticDoubleCS(staticIncrementCS(staticSquareCS(x)))
+ }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/StaticMethodCallIndyBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/StaticMethodCallIndyBench.java
new file mode 100644
index 0000000000..918fc3f71e
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/StaticMethodCallIndyBench.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.bench;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Benchmarks for the early {@code setTarget} optimisation on static method
+ * call sites in the invokedynamic dispatch path.
+ * <p>
+ * When the receiver of an {@code invokedynamic} call is a {@code Class} object
+ * and the resolved method is {@code static}, the call site target is set
+ * immediately — bypassing the normal hit-count threshold — so that the JIT
+ * compiler can inline the target method handle sooner.
+ * <p>
+ * This benchmark compares:
+ * <ul>
+ * <li><b>Java</b> — direct {@code invokestatic}, the theoretical optimum</li>
+ * <li><b>Groovy dynamic</b> — {@code invokedynamic} with early setTarget</li>
+ * <li><b>Groovy {@code @CompileStatic}</b> — direct {@code invokestatic}</li>
+ * <li><b>Groovy instance</b> — {@code invokedynamic} without early setTarget (control group)</li>
+ * </ul>
+ */
+@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(2)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+public class StaticMethodCallIndyBench {
+
+ private static final int SUM_N = 1000;
+ private static final int FIB_N = 25;
+ private static final int CHAIN_ITERATIONS = 1000;
+
+ private StaticMethodCallIndy instance;
+
+ @Setup
+ public void setUp() {
+ instance = new StaticMethodCallIndy();
+ }
+
+ // ---- loop with static call: sum(0..n) via repeated add ----
+
+ @Benchmark
+ public int staticSum_java() {
+ return JavaStaticMethods.sum(SUM_N);
+ }
+
+ @Benchmark
+ public int staticSum_groovy() {
+ return StaticMethodCallIndy.staticSum(SUM_N);
+ }
+
+ @Benchmark
+ public int staticSum_groovyCS() {
+ return StaticMethodCallIndy.staticSumCS(SUM_N);
+ }
+
+ @Benchmark
+ public int instanceSum_groovy() {
+ return instance.instanceSum(SUM_N);
+ }
+
+ // ---- recursive static call: Fibonacci ----
+
+ @Benchmark
+ public int staticFib_java() {
+ return JavaStaticMethods.fib(FIB_N);
+ }
+
+ @Benchmark
+ public int staticFib_groovy() {
+ return StaticMethodCallIndy.staticFib(FIB_N);
+ }
+
+ @Benchmark
+ public int staticFib_groovyCS() {
+ return StaticMethodCallIndy.staticFibCS(FIB_N);
+ }
+
+ @Benchmark
+ public int instanceFib_groovy() {
+ return instance.instanceFib(FIB_N);
+ }
+
+ // ---- chained static calls ----
+
+ @Benchmark
+ public int staticChain_java() {
+ int result = 0;
+ for (int i = 0; i < CHAIN_ITERATIONS; i++) {
+ result += JavaStaticMethods.chain(i);
+ }
+ return result;
+ }
+
+ @Benchmark
+ public int staticChain_groovy() {
+ int result = 0;
+ for (int i = 0; i < CHAIN_ITERATIONS; i++) {
+ result += StaticMethodCallIndy.staticChain(i);
+ }
+ return result;
+ }
+
+ @Benchmark
+ public int staticChain_groovyCS() {
+ int result = 0;
+ for (int i = 0; i < CHAIN_ITERATIONS; i++) {
+ result += StaticMethodCallIndy.staticChainCS(i);
+ }
+ return result;
+ }
+
+ @Benchmark
+ public int instanceChain_groovy() {
+ int result = 0;
+ for (int i = 0; i < CHAIN_ITERATIONS; i++) {
+ result += instance.instanceChain(i);
+ }
+ return result;
+ }
+
+ // ---- Java baseline implementations ----
+
+ private static class JavaStaticMethods {
+ static int add(int a, int b) {
+ return a + b;
+ }
+
+ static int sum(int n) {
+ int s = 0;
+ for (int i = 0; i < n; i++) {
+ s = add(s, i);
+ }
+ return s;
+ }
+
+ static int fib(int n) {
+ if (n < 2) return n;
+ return fib(n - 1) + fib(n - 2);
+ }
+
+ static int square(int x) { return x * x; }
+ static int increment(int x) { return x + 1; }
+ static int doubleIt(int x) { return x * 2; }
+
+ static int chain(int x) {
+ return doubleIt(increment(square(x)));
+ }
+ }
+}