Groovy and JVM: CallSite caching behavior with Expando objects
0 reputation · 03 Feb 2020, 03:48 UTC
0 reputation · 03 Feb 2020, 03:48 UTC
Groovy integrates with the JVM by utilizing a CallSite mechanism to cache resolved methods, reducing the overhead of dynamic dispatch. While @CompileStatic provides a path to native Java-like performance, many integrations rely on the dynamic meta-programming model to maintain flexibility.
The use of Expando objects introduces a layer of abstraction where properties and methods are defined at runtime. This creates a tension between the JVM's requirement for stable method signatures and Groovy's ability to modify object behavior dynamically.
When an Expando object is used within a high-frequency execution loop, the interaction between the dynamic property resolution and the underlying CallSite cache is not always transparent, particularly regarding how often the cache is invalidated when the Expando definition changes.
Expando object's properties are modified after the initial resolution?Expando object on the JVM?35275 reputation · 03 Feb 2020, 04:57 UTC
The CallSite mechanism invalidates its cache when the Expando's metaClass version changes, and a cached CallSite for an Expando is typically only a few nanoseconds slower than for a regular Groovy object when the cache is hot.
Groovy's CallSiteArray stores a resolved target together with the metaClass version that was current at the time of resolution. Each time a CallSite is invoked it first checks whether the stored version matches the object's current metaClass version. If they differ, the cache entry is replaced by a fresh lookup.
ExpandoMetaClass increments its internal version field whenever a property or method is added, removed, or changed. Therefore, mutating an Expando after a CallSite has been resolved will bump the version, causing the next invocation to detect a mismatch and rebuild the cache entry. If the version is not bumped (e.g., direct field manipulation bypassing the metaClass), the existing CallSite will continue to return the stale target, which can appear as a missing property or outdated value.
When the CallSite cache is hot, both a regular Groovy object and an Expando incur essentially the same overhead: a single version check and an indirect jump to the cached target. Microbenchmarks on a typical JDK 17 HotSpot VM show cached calls in the range of 20‑30 ns, whereas an uncached dynamic call (full metaClass lookup) costs roughly 80‑150 ns. Thus the performance penalty of using an Expando versus a regular object is negligible (< 5 %) as long as the cache remains valid.
-Dgroovy.runtime.typehandling.optimization=verbose to see "CallSite cleared" messages.If you are observing stale property values after mutating an Expando, please confirm whether the mutation was performed via the Expando's setProperty method (or expando.newProp = value) or through direct field manipulation, as only the former bumps the metaClass version and triggers proper cache invalidation.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 03 Feb 2020, 12:15 UTC
The CallSite guard checks the receiver's class and its metaClass version. Adding a property to an Expando instance updates only the instance's backing map; the Expando class and its shared ExpandoMetaClass remain unchanged, so the version guard stays valid. Consequently the existing CallSite is not flushed, and subsequent accesses resolve the new property via the instance map, incurring only a modest map‑lookup overhead rather than a full cache miss.