-
Notifications
You must be signed in to change notification settings - Fork 1.9k
GROOVY-10307: Improve invokedynamic performance with optimized caching #2374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: GROOVY_4_0_X
Are you sure you want to change the base?
Changes from all commits
510762d
74f0798
43d99f9
ca36b8c
8c05bd5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -32,7 +32,10 @@ | |||||||||||
| import java.lang.invoke.MethodType; | ||||||||||||
| import java.lang.invoke.MutableCallSite; | ||||||||||||
| import java.lang.invoke.SwitchPoint; | ||||||||||||
| import java.lang.ref.WeakReference; | ||||||||||||
| import java.util.Map; | ||||||||||||
| import java.util.Set; | ||||||||||||
| import java.util.concurrent.ConcurrentHashMap; | ||||||||||||
| import java.util.function.BiFunction; | ||||||||||||
| import java.util.function.Function; | ||||||||||||
| import java.util.logging.Level; | ||||||||||||
|
|
@@ -50,6 +53,13 @@ | |||||||||||
| public class IndyInterface { | ||||||||||||
| private static final long INDY_OPTIMIZE_THRESHOLD = SystemUtil.getLongSafe("groovy.indy.optimize.threshold", 10_000L); | ||||||||||||
| private static final long INDY_FALLBACK_THRESHOLD = SystemUtil.getLongSafe("groovy.indy.fallback.threshold", 10_000L); | ||||||||||||
| /** | ||||||||||||
| * Initial capacity for the call site registry used to track all active call sites | ||||||||||||
| * for cache invalidation when metaclass changes occur. The default of 1024 balances | ||||||||||||
| * memory usage against resize overhead. Tune via {@code groovy.indy.callsite.initial.capacity} | ||||||||||||
| * system property for larger applications. | ||||||||||||
| */ | ||||||||||||
| private static final int INDY_CALLSITE_INITIAL_CAPACITY = SystemUtil.getIntegerSafe("groovy.indy.callsite.initial.capacity", 1024); | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * flags for method and property calls | ||||||||||||
|
|
@@ -168,24 +178,70 @@ public static CallType fromCallSiteName(String callSiteName) { | |||||||||||
| } | ||||||||||||
|
|
||||||||||||
| protected static SwitchPoint switchPoint = new SwitchPoint(); | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Weak set of all CacheableCallSites. Used to invalidate caches when metaclass changes. | ||||||||||||
| * Uses WeakReferences so call sites can be garbage collected when no longer referenced. | ||||||||||||
| * <p> | ||||||||||||
| * Note: Stale (garbage-collected) WeakReferences are cleaned up during each call to | ||||||||||||
| * {@link #invalidateSwitchPoints()}. In applications with infrequent metaclass changes, | ||||||||||||
| * the set may accumulate some dead references between invalidations. This is acceptable | ||||||||||||
| * because: (1) the memory overhead per dead reference is minimal (~16 bytes), and | ||||||||||||
| * (2) frameworks like Grails that benefit most from this optimization have frequent | ||||||||||||
| * metaclass changes that trigger regular cleanup. | ||||||||||||
| */ | ||||||||||||
| private static final Set<WeakReference<CacheableCallSite>> ALL_CALL_SITES = ConcurrentHashMap.newKeySet(INDY_CALLSITE_INITIAL_CAPACITY); | ||||||||||||
|
|
||||||||||||
| static { | ||||||||||||
| GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu -> invalidateSwitchPoints()); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Register a call site for cache invalidation when metaclass changes. | ||||||||||||
| * <p> | ||||||||||||
| * Registered call sites are held via WeakReferences and will be automatically | ||||||||||||
| * removed when garbage collected. Cleanup of stale references occurs during | ||||||||||||
| * {@link #invalidateSwitchPoints()}. | ||||||||||||
| */ | ||||||||||||
| static void registerCallSite(CacheableCallSite callSite) { | ||||||||||||
| ALL_CALL_SITES.add(new WeakReference<>(callSite)); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Callback for constant metaclass update change | ||||||||||||
| * Callback for constant metaclass update change. | ||||||||||||
| * Invalidates all call site caches to ensure metaclass changes are visible. | ||||||||||||
| */ | ||||||||||||
| protected static void invalidateSwitchPoints() { | ||||||||||||
| if (LOG_ENABLED) { | ||||||||||||
| LOG.info("invalidating switch point"); | ||||||||||||
| LOG.info("invalidating switch point and call site caches"); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| synchronized (IndyInterface.class) { | ||||||||||||
| SwitchPoint old = switchPoint; | ||||||||||||
| switchPoint = new SwitchPoint(); | ||||||||||||
| SwitchPoint.invalidateAll(new SwitchPoint[]{old}); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Invalidate all call site caches and reset targets to default (cache lookup). | ||||||||||||
| // This ensures metaclass changes are visible without using expensive switchpoint guards. | ||||||||||||
| // Note: This is best-effort invalidation. A concurrent thread in fromCache() may briefly | ||||||||||||
| // reinstall an optimized target after we reset it. This is acceptable because the method | ||||||||||||
| // handle guards (SAME_MC, SAME_CLASS) will fail on the next call if the metaclass changed, | ||||||||||||
| // forcing a fallback to selectMethod() which will resolve the correct method. | ||||||||||||
| ALL_CALL_SITES.removeIf(ref -> { | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The size of
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Current implementation will clear all cache no matter whether the changed metaclasses are releated.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tested two versions of the parallel stream change with the two test application and saw a reduction in performance.
Approach 1: Parallel Collect + Atomic Swap - 18% SLOWER Approach 2: Parallel ForEach + Sequential RemoveIf - 12% SLOWER
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Testing with the Groovy 5 caller-specific Lookup pattern is generally positive on the micro benchmarks but 11% slower in the grails test application: More details: jamesfredley#1 |
||||||||||||
| CacheableCallSite cs = ref.get(); | ||||||||||||
| if (cs == null) { | ||||||||||||
| return true; // Remove garbage collected references | ||||||||||||
| } | ||||||||||||
| // Reset target to default (fromCache) so next call goes through cache lookup | ||||||||||||
| MethodHandle defaultTarget = cs.getDefaultTarget(); | ||||||||||||
| if (defaultTarget != null && cs.getTarget() != defaultTarget) { | ||||||||||||
| cs.setTarget(defaultTarget); | ||||||||||||
| } | ||||||||||||
| // Clear the cache so stale method handles are discarded | ||||||||||||
| cs.clearCache(); | ||||||||||||
| return false; | ||||||||||||
| }); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
|
|
@@ -230,6 +286,9 @@ private static CallSite realBootstrap(Lookup caller, String name, int callID, Me | |||||||||||
| mc.setTarget(mh); | ||||||||||||
| mc.setDefaultTarget(mh); | ||||||||||||
| mc.setFallbackTarget(makeFallBack(mc, sender, name, callID, type, safe, thisCall, spreadCall)); | ||||||||||||
|
|
||||||||||||
| // Register for cache invalidation on metaclass changes | ||||||||||||
| registerCallSite(mc); | ||||||||||||
|
|
||||||||||||
| return mc; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
Uh oh!
There was an error while loading. Please reload this page.