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/24 20:28:54 UTC
(groovy) branch master updated: GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining (#2473)
This is an automated email from the ASF dual-hosted git repository.
daniellansun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 815e7566be GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining (#2473)
815e7566be is described below
commit 815e7566be4658a44956db4e303c61cf77421bf0
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Apr 25 04:28:48 2026 +0800
GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining (#2473)
---
.../groovy/vmplugin/v8/CacheableCallSite.java | 3 +-
.../codehaus/groovy/vmplugin/v8/IndyInterface.java | 79 ++--
.../groovy/vmplugin/v8/MethodHandleWrapper.java | 16 +-
.../v8/IndyInterfaceCallSiteTargetTest.groovy | 499 +++++++++++++++++++++
.../groovy/bench/StaticMethodCallIndy.groovy | 124 +++++
.../groovy/bench/StaticMethodCallIndyBench.java | 180 ++++++++
6 files changed, 864 insertions(+), 37 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..77cef73e4a 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,49 @@ 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());
+ 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());
}
- } else {
- 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 +391,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 +418,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/IndyInterfaceCallSiteTargetTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy
new file mode 100644
index 0000000000..2a499a222a
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy
@@ -0,0 +1,499 @@
+/*
+ * 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.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 java.lang.reflect.Modifier
+import java.util.concurrent.atomic.AtomicInteger
+
+import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertNotSame
+import static org.junit.jupiter.api.Assertions.assertSame
+import static org.junit.jupiter.api.Assertions.assertTrue
+
+
+final class IndyInterfaceCallSiteTargetTest {
+
+ private String foo() {
+ 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'
+ }
+ }
+
+ 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' }
+ }
+
+ private static final class InstanceStaticCallTarget {
+ private static String valueOf(String value) { return "instance-static-$value" }
+ }
+
+ @Test
+ void testDeprecatedFromCacheRelinksTargetImmediatelyForStaticClassReceiver() {
+ MethodType type = MethodType.methodType(Object, Class)
+ CacheableCallSite callSite = newCallSite(type)
+ Object[] args = [IndyInterfaceCallSiteTargetTest] as Object[]
+
+ Object result = IndyInterface.fromCache(
+ callSite,
+ IndyInterfaceCallSiteTargetTest,
+ 'staticFoo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE,
+ Boolean.FALSE,
+ Boolean.FALSE,
+ 1,
+ args
+ )
+
+ assertEquals(staticFoo(), result)
+ assertNotSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleKeepsDefaultTargetForSpreadCall() {
+ MethodType type = MethodType.methodType(Object, Class, Object[])
+ CacheableCallSite callSite = newCallSite(type)
+ Object[] args = [IndyInterfaceCallSiteTargetTest, ['bar'] as Object[]] as Object[]
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite,
+ IndyInterfaceCallSiteTargetTest,
+ 'staticEcho',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ 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, IndyInterfaceCallSiteTargetTest)
+ CacheableCallSite callSite = newCallSite(type)
+ def receiver = new IndyInterfaceCallSiteTargetTest()
+ Object[] args = [receiver] as Object[]
+ MethodHandleWrapper wrapper = newCachedWrapper(
+ type, 'cached-instance-result', 'optimized-target-result',
+ CachedMethod.find(IndyInterfaceCallSiteTargetTest.getDeclaredMethod('foo')), true
+ )
+
+ cacheWrapper(callSite, receiver, wrapper)
+ primeLatestHitCount(callSite, receiver, wrapper, readIndyLong('INDY_OPTIMIZE_THRESHOLD'))
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceCallSiteTargetTest, '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 testFromCacheHandleLeavesDefaultTargetAfterFallbackCutoff() {
+ assertFallbackCutoffLeavesDefaultTarget(true)
+ assertFallbackCutoffLeavesDefaultTarget(false)
+ }
+
+ @Test
+ void testFromCacheHandleSkipsTargetChangesWhenCachedWrapperCannotSetTarget() {
+ MethodType type = MethodType.methodType(Object, IndyInterfaceCallSiteTargetTest)
+ CacheableCallSite callSite = newCallSite(type)
+ def receiver = new IndyInterfaceCallSiteTargetTest()
+ Object[] args = [receiver] as Object[]
+ MethodHandleWrapper wrapper = newCachedWrapper(type, 'uncacheable-result', 'ignored-target', null, false)
+
+ cacheWrapper(callSite, receiver, wrapper)
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceCallSiteTargetTest, 'foo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(wrapper.cachedMethodHandle, methodHandle)
+ assertSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleReturnsNullHandleForSafeNavigationReceiver() {
+ MethodType type = MethodType.methodType(Object, Object)
+ CacheableCallSite callSite = newCallSite(type)
+ Object[] args = [null] as Object[]
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceCallSiteTargetTest, '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 testFromCacheHandleUsesFallbackForPerInstanceMetaClassStaticReceiver() {
+ MethodType type = MethodType.methodType(Object, Class)
+ CacheableCallSite callSite = newCallSite(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, IndyInterfaceCallSiteTargetTest)
+ CacheableCallSite callSite = newCallSite(type)
+ def receiver = new IndyInterfaceCallSiteTargetTest()
+ Object[] args = [receiver] as Object[]
+ MethodHandleWrapper wrapper = newCachedWrapper(
+ type, 'already-linked-result', 'linked-target-result',
+ CachedMethod.find(IndyInterfaceCallSiteTargetTest.getDeclaredMethod('foo')), true
+ )
+
+ cacheWrapper(callSite, receiver, wrapper)
+ callSite.target = wrapper.targetMethodHandle
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceCallSiteTargetTest, 'foo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(wrapper.cachedMethodHandle, methodHandle)
+ assertSame(wrapper.targetMethodHandle, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleDoesNotRelinkWhenCallSiteParamIsObjectEvenIfReceiverIsClass() {
+ MethodType type = MethodType.methodType(Object, Object)
+ CacheableCallSite callSite = newCallSite(type)
+ Object[] args = [ClassA] as Object[]
+ 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',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(wrapper.cachedMethodHandle, methodHandle)
+ assertSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleDistinguishesDifferentClassReceivers() {
+ MethodType type = MethodType.methodType(Object, Class)
+ CacheableCallSite callSite = newCallSite(type)
+
+ Object[] argsA = [ClassA] as Object[]
+ MethodHandle handleA = invokeFromCacheHandle(
+ callSite, ClassA, 'bar',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsA
+ )
+ assertEquals(ClassA.bar(), handleA.invokeWithArguments([argsA] as Object[]))
+
+ Object[] argsB = [ClassB] as Object[]
+ MethodHandle handleB = invokeFromCacheHandle(
+ callSite, ClassB, 'bar',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsB
+ )
+ assertEquals(ClassB.bar(), handleB.invokeWithArguments([argsB] as Object[]))
+ }
+
+ @Test
+ void testFromCacheHandleRelinksImmediatelyForClassTypedCallSiteWithStaticMethod() {
+ MethodType type = MethodType.methodType(Object, Class)
+ CacheableCallSite callSite = newCallSite(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)
+ assertSame(wrapper.targetMethodHandle, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleKeepsDefaultTargetForClassTypedCallSiteWhenMethodMetadataIsMissing() {
+ MethodType type = MethodType.methodType(Object, Class)
+ CacheableCallSite callSite = newCallSite(type)
+ MethodHandleWrapper wrapper = newCachedWrapper(type, 'cached-class-result', 'ignored-target', null, 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)
+ assertSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleDoesNotRelinkStaticMethodInvokedThroughInstanceReceiver() {
+ MethodType type = MethodType.methodType(Object, InstanceStaticCallTarget, String)
+ CacheableCallSite callSite = newCallSite(type)
+ def receiver = new InstanceStaticCallTarget()
+ Object[] args = [receiver, 'abc'] as Object[]
+
+ MethodHandle selectedHandle = invokeSelectMethodHandle(
+ callSite, InstanceStaticCallTarget, 'valueOf',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+
+ assertEquals(InstanceStaticCallTarget.valueOf('abc'), selectedHandle.invokeWithArguments([args] as Object[]))
+ MethodHandleWrapper cachedWrapper = requireCachedWrapper(callSite, receiver)
+ assertTrue(Modifier.isStatic(cachedWrapper.method.modifiers))
+ assertSame(callSite.defaultTarget, callSite.target)
+
+ MethodHandle cachedHandle = invokeFromCacheHandle(
+ callSite, InstanceStaticCallTarget, 'valueOf',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(cachedWrapper.cachedMethodHandle, cachedHandle)
+ assertEquals(InstanceStaticCallTarget.valueOf('abc'), cachedHandle.invokeWithArguments([args] as Object[]))
+ assertSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testSelectMethodHandleCachesDistinctEntriesForDifferentClassReceivers() {
+ MethodType type = MethodType.methodType(Object, Class)
+ CacheableCallSite callSite = newCallSite(type)
+
+ Object[] argsA = [ClassA] as Object[]
+ MethodHandle handleA = invokeSelectMethodHandle(
+ callSite, ClassA, 'bar',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsA
+ )
+ assertEquals(ClassA.bar(), handleA.invokeWithArguments([argsA] as Object[]))
+
+ Object[] argsB = [ClassB] as Object[]
+ MethodHandle handleB = invokeSelectMethodHandle(
+ callSite, ClassB, 'bar',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsB
+ )
+ assertEquals(ClassB.bar(), handleB.invokeWithArguments([argsB] as Object[]))
+
+ MethodHandleWrapper cachedA = requireCachedWrapper(callSite, ClassA)
+ MethodHandleWrapper cachedB = requireCachedWrapper(callSite, ClassB)
+ assertNotSame(cachedA, cachedB)
+ assertEquals(ClassA.bar(), cachedA.cachedMethodHandle.invokeWithArguments([argsA] as Object[]))
+ assertEquals(ClassB.bar(), cachedB.cachedMethodHandle.invokeWithArguments([argsB] as Object[]))
+ assertSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testSelectMethodHandleStoresSentinelForUncacheableSpreadCall() {
+ MethodType type = MethodType.methodType(Object, Class, Object[])
+ CacheableCallSite callSite = newCallSite(type)
+ Object[] args = [IndyInterfaceCallSiteTargetTest, ['bar'] as Object[]] as Object[]
+
+ MethodHandle methodHandle = invokeSelectMethodHandle(
+ callSite, IndyInterfaceCallSiteTargetTest, 'staticEcho',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.TRUE, 1, args
+ )
+
+ assertEquals(staticEcho('bar'), methodHandle.invokeWithArguments([args] as Object[]))
+ assertSame(MethodHandleWrapper.getNullMethodHandleWrapper(), requireCachedWrapper(callSite, IndyInterfaceCallSiteTargetTest))
+ assertSame(callSite.defaultTarget, callSite.target)
+ }
+
+ private static CacheableCallSite newCallSite(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 void assertFallbackCutoffLeavesDefaultTarget(boolean startAwayFromDefaultTarget) {
+ MethodType type = MethodType.methodType(Object, IndyInterfaceCallSiteTargetTest)
+ CacheableCallSite callSite = newCallSite(type)
+ def receiver = new IndyInterfaceCallSiteTargetTest()
+ Object[] args = [receiver] as Object[]
+ MethodHandleWrapper wrapper = newCachedWrapper(
+ type, 'cached-instance-result', 'optimized-target-result',
+ CachedMethod.find(IndyInterfaceCallSiteTargetTest.getDeclaredMethod('foo')), true
+ )
+
+ cacheWrapper(callSite, receiver, wrapper)
+ primeLatestHitCount(callSite, receiver, wrapper, readIndyLong('INDY_OPTIMIZE_THRESHOLD'))
+ callSite.fallbackRound.set(readIndyLong('INDY_FALLBACK_CUTOFF') + 1L)
+ if (startAwayFromDefaultTarget) {
+ callSite.target = targetHandle(type, 'non-default-target')
+ }
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceCallSiteTargetTest, '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)
+ }
+
+ private static MethodHandleWrapper requireCachedWrapper(CacheableCallSite callSite, Object receiver) {
+ AtomicInteger providerCalls = new AtomicInteger()
+ MethodHandleWrapper wrapper = callSite.getAndPut(receiverClassName(receiver), { key ->
+ providerCalls.incrementAndGet()
+ MethodHandleWrapper.getNullMethodHandleWrapper()
+ })
+ assertEquals(0, providerCalls.get())
+ wrapper
+ }
+
+ 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
+ )
+ }
+
+ private static MethodHandle invokeSelectMethodHandle(CacheableCallSite callSite, Class<?> sender, String methodName,
+ int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) {
+ Method method = IndyInterface.getDeclaredMethod('selectMethodHandle',
+ 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)));
+ }
+ }
+}