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 <[email protected]>
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);
+    }
+}

Reply via email to