From 7989b700f652248af34b43bbc3874e1b85525268 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/CacheableCallSite.java | 18 +- .../groovy/vmplugin/v8/IndyInterface.java | 80 ++++++--- .../vmplugin/v8/MethodHandleWrapper.java | 68 ++++++++ .../v8/IndyInterfaceCallSiteTargetTest.groovy | 165 +++++++++++++++++- .../bench/FinalInstanceMethodCallIndy.groovy | 56 ++++++ .../FinalInstanceMethodCallIndyBench.java | 95 ++++++++++ .../FinalInstanceMethodCallIndyColdBench.java | 57 ++++++ .../PrivateInstanceMethodCallIndy.groovy | 61 +++++++ .../PrivateInstanceMethodCallIndyBench.java | 95 ++++++++++ ...rivateInstanceMethodCallIndyColdBench.java | 57 ++++++ 10 files changed, 717 insertions(+), 35 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/CacheableCallSite.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java index b6a9a4622c2..9ef47cc58fa 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -101,15 +101,19 @@ public MethodHandleWrapper getAndPut(String className, MemoizeCache.ValueProvide lruCache.put(className, resultSoftReference); } } - final SoftReference mhwsr = latestHitMethodHandleWrapperSoftReference; - final MethodHandleWrapper methodHandleWrapper = null == mhwsr ? null : mhwsr.get(); - - if (methodHandleWrapper == result) { + final SoftReference latestHitReference = latestHitMethodHandleWrapperSoftReference; + if (latestHitReference == resultSoftReference) { result.incrementLatestHitCount(); } else { - result.resetLatestHitCount(); - if (null != methodHandleWrapper) methodHandleWrapper.resetLatestHitCount(); - latestHitMethodHandleWrapperSoftReference = resultSoftReference; + final MethodHandleWrapper latestHitMethodHandleWrapper = null == latestHitReference ? null : latestHitReference.get(); + if (latestHitMethodHandleWrapper == result) { + result.incrementLatestHitCount(); + latestHitMethodHandleWrapperSoftReference = resultSoftReference; + } else { + result.resetLatestHitCount(); + if (null != latestHitMethodHandleWrapper) latestHitMethodHandleWrapper.resetLatestHitCount(); + latestHitMethodHandleWrapperSoftReference = resultSoftReference; + } } return result; 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..e54c3f1975f 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java @@ -30,7 +30,6 @@ 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; @@ -371,35 +370,69 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class mhw = fallbackSupplier.get(); } - 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 (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD) { - if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) { - if (callSite.getTarget() != callSite.getDefaultTarget()) { - // reset the call site target to default forever to avoid JIT deoptimization storm further - callSite.setTarget(callSite.getDefaultTarget()); + if (mhw.isCanSetTarget()) { + long latestHitCount = mhw.getLatestHitCount(); + boolean optimizeTarget = latestHitCount > INDY_OPTIMIZE_THRESHOLD; + boolean setTargetEarly = shouldSetCallSiteTargetEarly(mhw, receiver, latestHitCount); + + if (setTargetEarly || optimizeTarget) { + MethodHandle targetMethodHandle = mhw.getTargetMethodHandle(); + MethodHandle currentTarget = callSite.getTarget(); + if (currentTarget != targetMethodHandle) { + if (setTargetEarly) { + callSite.setTarget(targetMethodHandle); + currentTarget = targetMethodHandle; } - } else { - if (callSite.getTarget() != mhw.getTargetMethodHandle()) { - callSite.setTarget(mhw.getTargetMethodHandle()); - if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation"); + + if (optimizeTarget) { + if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) { + MethodHandle defaultTarget = callSite.getDefaultTarget(); + if (currentTarget != defaultTarget) { + // reset the call site target to default forever to avoid JIT deoptimization storm further + callSite.setTarget(defaultTarget); + } + } else { + if (currentTarget != targetMethodHandle) { + callSite.setTarget(targetMethodHandle); + if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation"); + } + } + + mhw.resetLatestHitCount(); } } - - mhw.resetLatestHitCount(); } } 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(MethodHandleWrapper mhw, Object receiver, long latestHitCount) { + if (mhw.shouldSetCallSiteTargetImmediately()) return true; + + return mhw.shouldSetCallSiteTargetOnRepeatedHit() + && latestHitCount > 0 + && receiver != null; + } + /** * Core method for indy method selection using runtime types. * @deprecated Use the new bootHandle-based approach instead. @@ -452,11 +485,12 @@ private static MethodHandleWrapper fallback(CacheableCallSite callSite, Class Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments); selector.setCallSiteTarget(); - return new MethodHandleWrapper( + return MethodHandleWrapper.create( selector.handle.asSpreader(Object[].class, arguments.length).asType(MethodType.methodType(Object.class, Object[].class)), selector.handle, selector.method, - selector.cache + selector.cache, + callSite.type().parameterType(0) ); } 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 71c3cb5dbb8..eb93e6baca3 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java @@ -21,6 +21,7 @@ import groovy.lang.MetaMethod; import java.lang.invoke.MethodHandle; +import java.lang.reflect.Modifier; import java.util.concurrent.atomic.AtomicLong; /** @@ -33,6 +34,7 @@ class MethodHandleWrapper { private final MethodHandle targetMethodHandle; private final MetaMethod method; private final boolean canSetTarget; + private final CallSiteTargetRelinkPolicy callSiteTargetRelinkPolicy; private final AtomicLong latestHitCount = new AtomicLong(0); /** @@ -44,10 +46,35 @@ class MethodHandleWrapper { * @param canSetTarget whether the call site target may be updated to this handle */ public MethodHandleWrapper(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, MetaMethod method, boolean canSetTarget) { + this(cachedMethodHandle, targetMethodHandle, method, canSetTarget, CallSiteTargetRelinkPolicy.NEVER); + } + + private MethodHandleWrapper(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, MetaMethod method, boolean canSetTarget, CallSiteTargetRelinkPolicy callSiteTargetRelinkPolicy) { this.cachedMethodHandle = cachedMethodHandle; this.targetMethodHandle = targetMethodHandle; this.method = method; this.canSetTarget = canSetTarget; + this.callSiteTargetRelinkPolicy = callSiteTargetRelinkPolicy; + } + + /** + * Creates a wrapper and precomputes the early call-site relink policy for the supplied receiver type. + * + * @param cachedMethodHandle the cached invocation handle + * @param targetMethodHandle the relink target handle + * @param method the associated meta method + * @param canSetTarget whether the call site target may be updated to this handle + * @param receiverType the declared call-site receiver type + * @return the configured wrapper + */ + static MethodHandleWrapper create(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, MetaMethod method, boolean canSetTarget, Class receiverType) { + return new MethodHandleWrapper( + cachedMethodHandle, + targetMethodHandle, + method, + canSetTarget, + resolveCallSiteTargetRelinkPolicy(method, receiverType) + ); } /** @@ -86,6 +113,24 @@ public boolean isCanSetTarget() { return canSetTarget; } + /** + * Indicates whether the call site can be relinked on the first cache hit. + * + * @return {@code true} when the target can be installed immediately + */ + boolean shouldSetCallSiteTargetImmediately() { + return callSiteTargetRelinkPolicy == CallSiteTargetRelinkPolicy.IMMEDIATE; + } + + /** + * Indicates whether the call site can be relinked after observing a repeated exact-final receiver hit. + * + * @return {@code true} when a repeated hit may trigger relinking + */ + boolean shouldSetCallSiteTargetOnRepeatedHit() { + return callSiteTargetRelinkPolicy == CallSiteTargetRelinkPolicy.AFTER_REPEATED_HIT; + } + /** * Increments the hit count for the latest inline-cache hit. * @@ -120,6 +165,29 @@ public static MethodHandleWrapper getNullMethodHandleWrapper() { return NullMethodHandleWrapper.INSTANCE; } + private static CallSiteTargetRelinkPolicy resolveCallSiteTargetRelinkPolicy(MetaMethod method, Class receiverType) { + if (method == null) return CallSiteTargetRelinkPolicy.NEVER; + + int modifiers = method.getModifiers(); + if (Modifier.isPrivate(modifiers)) return CallSiteTargetRelinkPolicy.IMMEDIATE; + if (Modifier.isStatic(modifiers)) { + return receiverType == Class.class + ? CallSiteTargetRelinkPolicy.IMMEDIATE + : CallSiteTargetRelinkPolicy.NEVER; + } + if (receiverType == Class.class) return CallSiteTargetRelinkPolicy.NEVER; + + return Modifier.isFinal(receiverType.getModifiers()) + ? CallSiteTargetRelinkPolicy.AFTER_REPEATED_HIT + : CallSiteTargetRelinkPolicy.NEVER; + } + + private enum CallSiteTargetRelinkPolicy { + NEVER, + IMMEDIATE, + AFTER_REPEATED_HIT + } + private static class NullMethodHandleWrapper extends MethodHandleWrapper { /** * Shared sentinel wrapper representing the absence of a reusable method handle. 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..98ab6317a56 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) } @@ -399,7 +554,7 @@ final class IndyInterfaceCallSiteTargetTest { } private static MethodHandleWrapper newCachedWrapper(MethodType type, Object cachedValue, Object targetValue, MetaMethod method, boolean canSetTarget) { - new MethodHandleWrapper(cachedHandle(cachedValue), targetHandle(type, targetValue), method, canSetTarget) + MethodHandleWrapper.create(cachedHandle(cachedValue), targetHandle(type, targetValue), method, canSetTarget, type.parameterType(0)) } private static MethodHandle cachedHandle(Object value) { 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); + } +}