From 5af418de2456619e55ab8b4b92d68f1087d13458 Mon Sep 17 00:00:00 2001 From: Daniel Sun Date: Sat, 23 May 2026 23:05:11 +0900 Subject: [PATCH] GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining(private and final cases) --- .../groovy/vmplugin/v8/IndyInterface.java | 55 +++++- .../v8/IndyInterfaceCallSiteTargetTest.groovy | 163 +++++++++++++++++- .../bench/FinalInstanceMethodCallIndy.groovy | 56 ++++++ .../FinalInstanceMethodCallIndyBench.java | 95 ++++++++++ .../FinalInstanceMethodCallIndyColdBench.java | 57 ++++++ .../PrivateInstanceMethodCallIndy.groovy | 61 +++++++ .../PrivateInstanceMethodCallIndyBench.java | 95 ++++++++++ ...rivateInstanceMethodCallIndyColdBench.java | 57 ++++++ 8 files changed, 629 insertions(+), 10 deletions(-) create mode 100644 subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/FinalInstanceMethodCallIndy.groovy create mode 100644 subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/FinalInstanceMethodCallIndyBench.java create mode 100644 subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/FinalInstanceMethodCallIndyColdBench.java create mode 100644 subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/PrivateInstanceMethodCallIndy.groovy create mode 100644 subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/PrivateInstanceMethodCallIndyBench.java create mode 100644 subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/PrivateInstanceMethodCallIndyColdBench.java 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): + *

    + *
  1. Private method (static or instance) — 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.
  2. + *
  3. Static call on a {@code Class} receiver — the dispatch target + * is fully determined by the declared call-site type; relink on first hit.
  4. + *
  5. Final receiver type — 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.
  6. + *
+ */ + 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 2a499a222aa..515d445815e 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 00000000000..334083b7342 --- /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}. + *

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