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/05/24 05:17:10 UTC
(groovy) 01/01: GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining(private and final cases)
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch GROOVY-11935-2
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 5af418de2456619e55ab8b4b92d68f1087d13458
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat May 23 23:05:11 2026 +0900
GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining(private and final cases)
---
.../codehaus/groovy/vmplugin/v8/IndyInterface.java | 55 ++++++-
.../v8/IndyInterfaceCallSiteTargetTest.groovy | 163 ++++++++++++++++++++-
.../bench/FinalInstanceMethodCallIndy.groovy | 56 +++++++
.../bench/FinalInstanceMethodCallIndyBench.java | 95 ++++++++++++
.../FinalInstanceMethodCallIndyColdBench.java | 57 +++++++
.../bench/PrivateInstanceMethodCallIndy.groovy | 61 ++++++++
.../bench/PrivateInstanceMethodCallIndyBench.java | 95 ++++++++++++
.../PrivateInstanceMethodCallIndyColdBench.java | 57 +++++++
8 files changed, 629 insertions(+), 10 deletions(-)
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 c088992337..6fbe331c02 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
@@ -372,12 +372,8 @@ public class IndyInterface {
}
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 (shouldSetCallSiteTargetEarly(callSite, mhw, receiver)) {
+ callSite.setTarget(mhw.getTargetMethodHandle());
}
if (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD) {
@@ -400,6 +396,53 @@ public class IndyInterface {
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(CacheableCallSite callSite, MethodHandleWrapper mhw, Object receiver) {
+ var method = mhw.getMethod();
+ if (method == null) return false;
+ int modifiers = method.getModifiers();
+
+ // Private method (static or instance): non-overridable; the target is uniquely determined
+ // and cannot change through subclassing, so relinking is safe on the very first hit.
+ if (Modifier.isPrivate(modifiers)) return true;
+
+ // Static call: stable only when the call-site declared type is Class,
+ // because that is the only shape where the dispatch target is fully determined by the
+ // declared type alone (different Class objects yield different static-method targets).
+ Class<?> receiverType = callSite.type().parameterType(0);
+ if (Modifier.isStatic(modifiers)) return receiverType == Class.class;
+
+ // Require at least one repeated hit for non-private, non-static sites to filter cold invocations.
+ if (mhw.getLatestHitCount() == 0) return false;
+
+ // Null and Class<?> receivers must be excluded: null has no verifier-enforced type, and a Class<?> instance
+ // used as an instance-method receiver dispatches through Class metaclass machinery — neither maps
+ // cleanly to the declared receiverType, so early relinking would corrupt future invocations.
+ if (receiver == null || receiver instanceof Class<?>) return false;
+
+ // Final receiver type: a final class has no subclasses, relinking is safe as soon as the site is locally warm
+ // (latestHitCount > 0, enforced above), because the dispatch target can never change due
+ // to a receiver-type shift.
+ return Modifier.isFinal(receiverType.getModifiers());
+ }
+
/**
* Core method for indy method selection using runtime types.
* @deprecated Use the new bootHandle-based approach instead.
diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy
index 2a499a222a..515d445815 100644
--- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy
@@ -42,6 +42,10 @@ final class IndyInterfaceCallSiteTargetTest {
return 'foo-result'
}
+ protected String protectedFoo() {
+ return 'protected-foo-result'
+ }
+
private static String staticFoo() {
return 'static-foo-result'
}
@@ -58,14 +62,24 @@ final class IndyInterfaceCallSiteTargetTest {
private static final class ClassA {
private static String bar() { return 'bar-from-A' }
+ static String baz() { return 'baz-from-A' }
}
private static final class ClassB {
private static String bar() { return 'bar-from-B' }
+ static String baz() { return 'baz-from-B' }
}
private static final class InstanceStaticCallTarget {
private static String valueOf(String value) { return "instance-static-$value" }
+ static String visibleValueOf(String value) { return "instance-visible-static-$value" }
+ }
+
+ private static class PrivateMethodBase {
+ private String hidden() { return 'hidden-from-base' }
+ }
+
+ private static final class PrivateMethodChild extends PrivateMethodBase {
}
@Test
@@ -90,6 +104,29 @@ final class IndyInterfaceCallSiteTargetTest {
assertNotSame(callSite.defaultTarget, callSite.target)
}
+ @Test
+ void testDeprecatedFromCacheRelinksTargetImmediatelyForPrivateMethod() {
+ MethodType type = MethodType.methodType(Object, Object)
+ CacheableCallSite callSite = newCallSite(type)
+ def receiver = new IndyInterfaceCallSiteTargetTest()
+ Object[] args = [receiver] as Object[]
+
+ Object result = IndyInterface.fromCache(
+ callSite,
+ IndyInterfaceCallSiteTargetTest,
+ 'foo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE,
+ Boolean.TRUE,
+ Boolean.FALSE,
+ 1,
+ args
+ )
+
+ assertEquals(receiver.foo(), result)
+ assertNotSame(callSite.defaultTarget, callSite.target)
+ }
+
@Test
void testFromCacheHandleKeepsDefaultTargetForSpreadCall() {
MethodType type = MethodType.methodType(Object, Class, Object[])
@@ -137,6 +174,95 @@ final class IndyInterfaceCallSiteTargetTest {
assertEquals(0L, wrapper.latestHitCount)
}
+ @Test
+ void testFromCacheHandleRelinksImmediatelyForPrivateMethodEvenWithGenericCallSiteType() {
+ MethodType type = MethodType.methodType(Object, Object)
+ CacheableCallSite callSite = newCallSite(type)
+ def receiver = new IndyInterfaceCallSiteTargetTest()
+ Object[] args = [receiver] as Object[]
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceCallSiteTargetTest, 'foo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args
+ )
+
+ assertEquals(receiver.foo(), methodHandle.invokeWithArguments([args] as Object[]))
+ assertNotSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleRelinksImmediatelyForPrivateMethodOnSubclassReceiver() {
+ MethodType type = MethodType.methodType(Object, Object)
+ CacheableCallSite callSite = newCallSite(type)
+ def receiver = new PrivateMethodChild()
+ Object[] args = [receiver] as Object[]
+
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, PrivateMethodBase, 'hidden',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args
+ )
+
+ assertEquals('hidden-from-base', methodHandle.invokeWithArguments([args] as Object[]))
+ assertNotSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleRelinksExactFinalReceiverAfterRepeatedHit() {
+ MethodType type = MethodType.methodType(Object, IndyInterfaceCallSiteTargetTest)
+ CacheableCallSite callSite = newCallSite(type)
+ def receiver = new IndyInterfaceCallSiteTargetTest()
+ Object[] args = [receiver] as Object[]
+ MethodHandleWrapper wrapper = newCachedWrapper(
+ type, 'cached-final-result', 'final-target-result',
+ CachedMethod.find(IndyInterfaceCallSiteTargetTest.getDeclaredMethod('protectedFoo')), true
+ )
+
+ cacheWrapper(callSite, receiver, wrapper)
+
+ MethodHandle firstHit = invokeFromCacheHandle(
+ callSite, IndyInterfaceCallSiteTargetTest, 'protectedFoo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+ assertSame(wrapper.cachedMethodHandle, firstHit)
+ assertSame(callSite.defaultTarget, callSite.target)
+
+ MethodHandle secondHit = invokeFromCacheHandle(
+ callSite, IndyInterfaceCallSiteTargetTest, 'protectedFoo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+ assertSame(wrapper.cachedMethodHandle, secondHit)
+ assertSame(wrapper.targetMethodHandle, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleDoesNotRelinkFinalReceiverWhenCallSiteTypeIsNotExact() {
+ MethodType type = MethodType.methodType(Object, Object)
+ CacheableCallSite callSite = newCallSite(type)
+ def receiver = new IndyInterfaceCallSiteTargetTest()
+ Object[] args = [receiver] as Object[]
+ MethodHandleWrapper wrapper = newCachedWrapper(
+ type, 'cached-object-result', 'ignored-object-target',
+ CachedMethod.find(IndyInterfaceCallSiteTargetTest.getDeclaredMethod('protectedFoo')), true
+ )
+
+ cacheWrapper(callSite, receiver, wrapper)
+
+ 2.times {
+ MethodHandle methodHandle = invokeFromCacheHandle(
+ callSite, IndyInterfaceCallSiteTargetTest, 'protectedFoo',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+ assertSame(wrapper.cachedMethodHandle, methodHandle)
+ }
+
+ assertSame(callSite.defaultTarget, callSite.target)
+ }
+
@Test
void testFromCacheHandleLeavesDefaultTargetAfterFallbackCutoff() {
assertFallbackCutoffLeavesDefaultTarget(true)
@@ -230,19 +356,19 @@ final class IndyInterfaceCallSiteTargetTest {
}
@Test
- void testFromCacheHandleDoesNotRelinkWhenCallSiteParamIsObjectEvenIfReceiverIsClass() {
+ void testFromCacheHandleDoesNotRelinkWhenCallSiteParamIsObjectEvenIfReceiverIsClassForNonPrivateStaticMethod() {
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
+ CachedMethod.find(ClassA.getDeclaredMethod('baz')), true
)
cacheWrapper(callSite, ClassA, wrapper)
MethodHandle methodHandle = invokeFromCacheHandle(
- callSite, ClassA, 'bar',
+ callSite, ClassA, 'baz',
IndyInterface.CallType.METHOD.getOrderNumber(),
Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
)
@@ -315,7 +441,7 @@ final class IndyInterfaceCallSiteTargetTest {
}
@Test
- void testFromCacheHandleDoesNotRelinkStaticMethodInvokedThroughInstanceReceiver() {
+ void testFromCacheHandleRelinksImmediatelyForPrivateStaticMethodInvokedThroughInstanceReceiver() {
MethodType type = MethodType.methodType(Object, InstanceStaticCallTarget, String)
CacheableCallSite callSite = newCallSite(type)
def receiver = new InstanceStaticCallTarget()
@@ -340,6 +466,35 @@ final class IndyInterfaceCallSiteTargetTest {
assertSame(cachedWrapper.cachedMethodHandle, cachedHandle)
assertEquals(InstanceStaticCallTarget.valueOf('abc'), cachedHandle.invokeWithArguments([args] as Object[]))
+ assertNotSame(callSite.defaultTarget, callSite.target)
+ }
+
+ @Test
+ void testFromCacheHandleDoesNotRelinkNonPrivateStaticMethodInvokedThroughInstanceReceiver() {
+ 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, 'visibleValueOf',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+
+ assertEquals(InstanceStaticCallTarget.visibleValueOf('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, 'visibleValueOf',
+ IndyInterface.CallType.METHOD.getOrderNumber(),
+ Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args
+ )
+
+ assertSame(cachedWrapper.cachedMethodHandle, cachedHandle)
+ assertEquals(InstanceStaticCallTarget.visibleValueOf('abc'), cachedHandle.invokeWithArguments([args] as Object[]))
assertSame(callSite.defaultTarget, callSite.target)
}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/FinalInstanceMethodCallIndy.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/FinalInstanceMethodCallIndy.groovy
new file mode 100644
index 0000000000..334083b734
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/FinalInstanceMethodCallIndy.groovy
@@ -0,0 +1,56 @@
+/*
+ * 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
+
+/**
+ * Final-instance counterpart to {@link StaticMethodCallIndy}.
+ * <p>
+ * The receiver type is exact and final at the indy call site, which makes it a good probe for
+ * earlier relink heuristics that are still too broad to fire on the first hit.
+ */
+final class FinalInstanceMethodCallIndy {
+
+ 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)))
+ }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/FinalInstanceMethodCallIndyBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/FinalInstanceMethodCallIndyBench.java
new file mode 100644
index 0000000000..faad33eca1
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/FinalInstanceMethodCallIndyBench.java
@@ -0,0 +1,95 @@
+/*
+ * 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 exact-final receiver call sites independently of the static-method benchmarks.
+ */
+@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 FinalInstanceMethodCallIndyBench {
+
+ private static final int SUM_N = 1000;
+ private static final int FIB_N = 25;
+ private static final int CHAIN_ITERATIONS = 1000;
+
+ private FinalInstanceMethodCallIndy finalInstance;
+ private StaticMethodCallIndy instance;
+
+ @Setup
+ public void setUp() {
+ finalInstance = new FinalInstanceMethodCallIndy();
+ instance = new StaticMethodCallIndy();
+ }
+
+ @Benchmark
+ public int finalInstanceSum_groovy() {
+ return finalInstance.instanceSum(SUM_N);
+ }
+
+ @Benchmark
+ public int instanceSum_groovy() {
+ return instance.instanceSum(SUM_N);
+ }
+
+ @Benchmark
+ public int finalInstanceFib_groovy() {
+ return finalInstance.instanceFib(FIB_N);
+ }
+
+ @Benchmark
+ public int instanceFib_groovy() {
+ return instance.instanceFib(FIB_N);
+ }
+
+ @Benchmark
+ public int finalInstanceChain_groovy() {
+ int result = 0;
+ for (int i = 0; i < CHAIN_ITERATIONS; i++) {
+ result += finalInstance.instanceChain(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;
+ }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/FinalInstanceMethodCallIndyColdBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/FinalInstanceMethodCallIndyColdBench.java
new file mode 100644
index 0000000000..70a24068a8
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/FinalInstanceMethodCallIndyColdBench.java
@@ -0,0 +1,57 @@
+/*
+ * 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.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Cold-start benchmark for exact-final receiver call sites.
+ */
+@Warmup(iterations = 0)
+@Measurement(iterations = 1, batchSize = 1)
+@Fork(80)
+@BenchmarkMode(Mode.SingleShotTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@State(Scope.Thread)
+public class FinalInstanceMethodCallIndyColdBench {
+
+ @Param({"500", "2000", "20000"})
+ public int n;
+
+ @Benchmark
+ public int finalInstanceSum_groovy() {
+ return new FinalInstanceMethodCallIndy().instanceSum(n);
+ }
+
+ @Benchmark
+ public int instanceSum_groovy() {
+ return new StaticMethodCallIndy().instanceSum(n);
+ }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/PrivateInstanceMethodCallIndy.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/PrivateInstanceMethodCallIndy.groovy
new file mode 100644
index 0000000000..e4062c021b
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/PrivateInstanceMethodCallIndy.groovy
@@ -0,0 +1,61 @@
+/*
+ * 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
+
+/**
+ * Private-method counterpart to {@link StaticMethodCallIndy}.
+ * <p>
+ * Dispatch stays dynamic at the call site, but the selected target is lexically fixed because
+ * the helper methods are {@code private}. This makes it a good probe for eager relink decisions
+ * that should not require an exact-final receiver type.
+ */
+class PrivateInstanceMethodCallIndy {
+
+ private 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
+ }
+
+ private int instanceFib0(int n) {
+ if (n < 2) return n
+ return instanceFib0(n - 1) + instanceFib0(n - 2)
+ }
+
+ int instanceFib(int n) {
+ return instanceFib0(n)
+ }
+
+ private int instanceSquare(int x) { return x * x }
+
+ private int instanceIncrement(int x) { return x + 1 }
+
+ private int instanceDouble(int x) { return x * 2 }
+
+ int instanceChain(int x) {
+ return instanceDouble(instanceIncrement(instanceSquare(x)))
+ }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/PrivateInstanceMethodCallIndyBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/PrivateInstanceMethodCallIndyBench.java
new file mode 100644
index 0000000000..a9bcaeb068
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/PrivateInstanceMethodCallIndyBench.java
@@ -0,0 +1,95 @@
+/*
+ * 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 private-method call sites independently of the static-method benchmarks.
+ */
+@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 PrivateInstanceMethodCallIndyBench {
+
+ private static final int SUM_N = 1000;
+ private static final int FIB_N = 25;
+ private static final int CHAIN_ITERATIONS = 1000;
+
+ private PrivateInstanceMethodCallIndy privateInstance;
+ private StaticMethodCallIndy instance;
+
+ @Setup
+ public void setUp() {
+ privateInstance = new PrivateInstanceMethodCallIndy();
+ instance = new StaticMethodCallIndy();
+ }
+
+ @Benchmark
+ public int privateInstanceSum_groovy() {
+ return privateInstance.instanceSum(SUM_N);
+ }
+
+ @Benchmark
+ public int instanceSum_groovy() {
+ return instance.instanceSum(SUM_N);
+ }
+
+ @Benchmark
+ public int privateInstanceFib_groovy() {
+ return privateInstance.instanceFib(FIB_N);
+ }
+
+ @Benchmark
+ public int instanceFib_groovy() {
+ return instance.instanceFib(FIB_N);
+ }
+
+ @Benchmark
+ public int privateInstanceChain_groovy() {
+ int result = 0;
+ for (int i = 0; i < CHAIN_ITERATIONS; i++) {
+ result += privateInstance.instanceChain(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;
+ }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/PrivateInstanceMethodCallIndyColdBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/PrivateInstanceMethodCallIndyColdBench.java
new file mode 100644
index 0000000000..6acb9ad620
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/PrivateInstanceMethodCallIndyColdBench.java
@@ -0,0 +1,57 @@
+/*
+ * 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.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Cold-start benchmark for private-method call sites.
+ */
+@Warmup(iterations = 0)
+@Measurement(iterations = 1, batchSize = 1)
+@Fork(80)
+@BenchmarkMode(Mode.SingleShotTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@State(Scope.Thread)
+public class PrivateInstanceMethodCallIndyColdBench {
+
+ @Param({"500", "2000", "20000"})
+ public int n;
+
+ @Benchmark
+ public int privateInstanceSum_groovy() {
+ return new PrivateInstanceMethodCallIndy().instanceSum(n);
+ }
+
+ @Benchmark
+ public int instanceSum_groovy() {
+ return new StaticMethodCallIndy().instanceSum(n);
+ }
+}