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/18 13:02:40 UTC

(groovy) 01/01: GROOVY-11935: Set invokedynamic call site target immediately for static method calls to enable earlier JIT inlining

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch GROOVY-11935
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 95d0336bb2d21d1cc9f72f2a91ea9e6c98cc0376
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Apr 18 21:33:34 2026 +0900

    GROOVY-11935: Set invokedynamic call site target immediately for static method calls to enable earlier JIT inlining
---
 .../codehaus/groovy/vmplugin/v8/IndyInterface.java |  13 +-
 .../groovy/vmplugin/v8/MethodHandleWrapper.java    |  16 ++-
 .../groovy/bench/StaticMethodCallIndy.groovy       |  97 +++++++++++++
 .../groovy/bench/StaticMethodCallIndyBench.java    | 157 +++++++++++++++++++++
 4 files changed, 277 insertions(+), 6 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 91682cc722..995f21cd38 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
@@ -31,6 +31,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,14 +321,13 @@ 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);
+        final Object receiver = arguments[0];
 
         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;
@@ -339,6 +339,14 @@ public class IndyInterface {
             mhw = fallbackSupplier.get();
         }
 
+        // GROOVY-11935: Set invokedynamic call site target immediately for static method calls to enable earlier JIT inlining
+        if (receiver instanceof Class) {
+            var method = mhw.getMethod();
+            if (method != null && Modifier.isStatic(method.getModifiers())) {
+                callSite.setTarget(mhw.getTargetMethodHandle());
+            }
+        }
+
         if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle()) && (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD)) {
             if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) {
                 if (callSite.getTarget() != callSite.getDefaultTarget()) {
@@ -404,6 +412,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/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..21e49a7c2c
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/StaticMethodCallIndy.groovy
@@ -0,0 +1,97 @@
+/*
+ *  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
+    }
+
+    // ---------- @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)
+    }
+}
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..4cc4deef25
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/StaticMethodCallIndyBench.java
@@ -0,0 +1,157 @@
+/*
+ *  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);
+    }
+
+    // ---- 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;
+    }
+
+    // ---- 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)));
+        }
+    }
+}