GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining#2473
GROOVY-11935: Set invokedynamic call site target immediately to enable earlier JIT inlining#2473daniellansun wants to merge 1 commit intomasterfrom
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2473 +/- ##
==================================================
+ Coverage 67.1472% 67.3214% +0.1742%
- Complexity 30944 33513 +2569
==================================================
Files 1438 1451 +13
Lines 120148 128714 +8566
Branches 21311 24107 +2796
==================================================
+ Hits 80676 86652 +5976
- Misses 32724 34904 +2180
- Partials 6748 7158 +410
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Implements GROOVY-11935 by adjusting the invokedynamic dispatch path to relink static method call sites earlier (to enable earlier JIT inlining), and adds a dedicated JMH benchmark to measure the impact.
Changes:
- Add early
MutableCallSite.setTargetfor static method calls when the receiver is aClass. - Extend
MethodHandleWrapperto carry the resolvedMetaMethodso static-ness can be detected at the call site. - Introduce new JMH benchmarks (
StaticMethodCallIndyBench+ Groovy helper) to compare Java vs Groovy dynamic vs@CompileStatic.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/StaticMethodCallIndyBench.java | New JMH benchmark targeting early relinking for static indy calls. |
| subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/StaticMethodCallIndy.groovy | Helper methods (dynamic, instance, and @CompileStatic) used by the new benchmark. |
| src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java | Adds MetaMethod storage/access so callers can inspect modifiers (e.g., static). |
| src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java | Adds GROOVY-11935 early relinking logic and passes selector.method into MethodHandleWrapper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // GROOVY-11935: Set invokedynamic call site target immediately for static method calls to enable earlier JIT inlining | ||
| if (receiver instanceof Class) { | ||
| var method = mhw.getMethod(); | ||
| if (method != null && Modifier.isStatic(method.getModifiers())) { | ||
| callSite.setTarget(mhw.getTargetMethodHandle()); | ||
| } | ||
| } |
There was a problem hiding this comment.
The new GROOVY-11935 behavior changes call-site relinking (static calls can now update the MutableCallSite target immediately). There are existing tests for IndyInterface’s deprecated entry points, but none appear to assert the new static-call relinking behavior or that it only happens when caching/guards are enabled. Please add a regression test that exercises a static call through IndyInterface.fromCache/fromCacheHandle and verifies the call site target is (or is not) updated as intended.
| if (receiver instanceof Class) { | ||
| var method = mhw.getMethod(); | ||
| if (method != null && Modifier.isStatic(method.getModifiers())) { | ||
| callSite.setTarget(mhw.getTargetMethodHandle()); |
There was a problem hiding this comment.
The early callSite.setTarget() path for static calls does not check mhw.isCanSetTarget(). If Selector.cache is false (e.g., UNCACHED_CALL / guardless paths), this can install an unguarded target handle and bypass switchpoint/arg guards, changing semantics and potentially breaking invalidation. Gate this optimization on mhw.isCanSetTarget() (and ideally also avoid resetting the target if it’s already the desired one).
| if (receiver instanceof Class) { | |
| var method = mhw.getMethod(); | |
| if (method != null && Modifier.isStatic(method.getModifiers())) { | |
| callSite.setTarget(mhw.getTargetMethodHandle()); | |
| if (mhw.isCanSetTarget() && receiver instanceof Class) { | |
| var method = mhw.getMethod(); | |
| if (method != null && Modifier.isStatic(method.getModifiers())) { | |
| var targetMethodHandle = mhw.getTargetMethodHandle(); | |
| if (callSite.getTarget() != targetMethodHandle) { | |
| callSite.setTarget(targetMethodHandle); | |
| } |
| * This benchmark compares: | ||
| * <ul> | ||
| * <li><b>Java</b> — direct {@code invokestatic}, the theoretical optimum</li> | ||
| * <li><b>Groovy dynamic</b> — {@code invokedynamic} with early setTarget</li> | ||
| * <li><b>Groovy {@code @CompileStatic}</b> — direct {@code invokestatic}</li> | ||
| * <li><b>Groovy instance</b> — {@code invokedynamic} without early setTarget (control group)</li> | ||
| * </ul> |
There was a problem hiding this comment.
This Javadoc claims the benchmark compares 4 variants (including Groovy instance control group and a Groovy @CompileStatic baseline), but the actual benchmark methods don’t cover all variants for each scenario (e.g., no instance Fibonacci benchmark and no @CompileStatic chained-call benchmark). Please align the list with what’s actually measured, or add the missing benchmark methods so the documentation matches the results.
| * This benchmark compares: | |
| * <ul> | |
| * <li><b>Java</b> — direct {@code invokestatic}, the theoretical optimum</li> | |
| * <li><b>Groovy dynamic</b> — {@code invokedynamic} with early setTarget</li> | |
| * <li><b>Groovy {@code @CompileStatic}</b> — direct {@code invokestatic}</li> | |
| * <li><b>Groovy instance</b> — {@code invokedynamic} without early setTarget (control group)</li> | |
| * </ul> | |
| * The benchmarks in this class cover selected comparisons between: | |
| * <ul> | |
| * <li><b>Java</b> — direct {@code invokestatic}, the theoretical optimum</li> | |
| * <li><b>Groovy dynamic</b> — {@code invokedynamic} with early setTarget</li> | |
| * <li><b>Groovy {@code @CompileStatic}</b> — direct {@code invokestatic}</li> | |
| * <li><b>Groovy instance</b> — {@code invokedynamic} without early setTarget (control group)</li> | |
| * </ul> | |
| * Not every scenario includes every variant; each benchmark method measures | |
| * only the implementations defined for that specific case. |
eef7dfb to
3c7055f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
df2f9f7 to
bd517da
Compare
| */ | ||
| private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class<?> sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable { | ||
| FallbackSupplier fallbackSupplier = new FallbackSupplier(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments); | ||
| final Object receiver = arguments[0]; |
There was a problem hiding this comment.
So what will happen in this case?
def foo(receiver){receiver.bar()}
class A{ static bar(){1} }
class B{ static bar(){2} }
foo(A)
foo(B)
foo(new A())
foo(new B())
for the first two calls the class is the same, since receiver.getClass will return Class for both. But that is imho a bug, that existed already before callSite.getAndPut(receiverClassName,.... Also I think the later two cases are not considered here... why not? If we really only want to optimize A.bar() style calls, then we should check the receiver type through the callsite type, not the runtime type.
bd517da to
42bd244
Compare
e8bb30f to
a13afa5
Compare
…e earlier JIT inlining
a13afa5 to
28da458
Compare
|
If no one is opposed to the PR, I will merge it at this weekend. |
https://issues.apache.org/jira/browse/GROOVY-11935