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)));
+ }
+ }
+}