Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 49 additions & 6 deletions src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.
*
* <p>Three cases trigger early relinking (in priority order):
* <ol>
* <li><b>Private method (static or instance)</b> — 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.</li>
* <li><b>Static call on a {@code Class} receiver</b> — the dispatch target
* is fully determined by the declared call-site type; relink on first hit.</li>
* <li><b>Final receiver type</b> — 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.</li>
* </ol>
*/
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand All @@ -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
Expand All @@ -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[])
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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()
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
* <p>
* 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)))
}
}
Loading
Loading