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 c0889923370..6fbe331c022 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 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class> } 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 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class> return mhw.getCachedMethodHandle(); } + /** + * GROOVY-11935: install direct-looking targets early when the receiver shape is already + * specific enough to make earlier JIT inlining worthwhile. + * + *
Three cases trigger early relinking (in priority order): + *
+ * 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 00000000000..faad33eca1b --- /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 00000000000..70a24068a82 --- /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 00000000000..e4062c021b9 --- /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}. + *
+ * 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 00000000000..a9bcaeb068c --- /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 00000000000..6acb9ad6200 --- /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); + } +}