createObject/new fast-path: kill exception-driven control flow + cache hot lookups#2800
Open
zspitzer wants to merge 1 commit into
Open
createObject/new fast-path: kill exception-driven control flow + cache hot lookups#2800zspitzer wants to merge 1 commit into
zspitzer wants to merge 1 commit into
Conversation
…, add class/ctor/interfaces/bootdelegation caches, FQN short-circuit
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Removes two exception-driven control-flow anti-patterns from the
createObject("java", ...)/new SomeJavaClass()paths and adds severalhot-path caches. Profiled on Lucee 7.1 with a 2M-iteration
createObject/new java.lang.StringBuilder()bench under JFR(
jfc-soak.jfc).Throws eliminated
new SomeJavaClass()was throwingExpressionException: "file or directory [some/java/Class.cfc] does not exist"on every call.
ComponentLoader._searchcallsResourceUtil.toResourceExisting(..., defaultValue)to "safely" probefor the CFC file — the inner implementation throws when the file
doesn't exist and the outer catches and returns the default. Refactored
ResourceUtil.toResourceExistingfamily to share a non-throwing_findExistinghelper; the throw is now reserved for the genuinelyno-defaultValue overload.
new SomeJavaClass( arg )was also throwingApplicationException: "can't convert [java.lang.Long] to [java.lang.CharSequence]"for each non-matching constructor overload during selection.
Reflector._convertthrew on no-match, andClazz.getConstructor/getMethodcaught and skipped to the next overload. Introduced astatic
Reflector.UNCONVERTIBLEsentinel andconvertSafemethodso overload selection iterates via return value rather than exception.
Behaviour of the throwing public
_convertunchanged.In the bench, JFR-captured exception throws inside the hot loop dropped
from 6,011,340 to 0.
Caches added (all keyed via JDK
ClassValuewhere appropriate)OSGiUtil.isClassInBootelegation/isPackageInBootelegation— waswalking the boot-delegation list with
String.substring/String.startsWithon every call. Boot-delegation list is loadedonce and never changes, so the result is now cached in a
ConcurrentHashMap<String, Boolean>.ConfigImpl—getRPCClassLoader(reload, js)was rebuilding acomposite cache key (
HashUtil.create64BitHashAsString+Long.toString)on every call before dispatching to the existing factory cache. Added
a
volatile ClassLoader defaultRpcClassLoaderfield that short-circuitswhen
js == null || js == getJavaSettings(). Invalidated inclearRPCClassLoader()andresetJavaSettings().Reflector—ClassValue<Class[]> INTERFACES_CACHEreplaces directClass.getInterfaces()calls in both_checkInterfacesoverloads.The JDK clones the interfaces array per call;
ClassValueis thepurpose-built mechanism and survives class unload correctly.
ClassUtil—ClassValue<Constructor> NO_ARG_CONSTRUCTORcaches theresolved no-arg constructor per class.
ClassValue<ConcurrentHashMap<Class, Constructor>> ONE_ARG_CONSTRUCTOR_CACHEcaches single-arg lookupskeyed by
args[0].getClass(). Cache hits convertargs[0]explicitlyvia
Reflector.convertSafeagainst the cached parameter type, thencall
Constructor.newInstancedirectly — bypassing theReflector.getConstructorInstance+Clazz.getConstructoriteration.JavaProxy—ConcurrentHashMap<ClassLoader, ConcurrentHashMap<String, Class<?>>> CLASS_CACHEfor resolved class lookups when nopath/bundleNameis supplied. HelpertryCachedLoad/cacheClassLookupexposed so_CreateComponent.loadClassshares thesame cache on the
newpath.Other
_CreateComponent— whenpathstarts withjava./javax./jakarta./sun./jdk./com.sun./org.w3c./org.xml.,skip the
ComponentLoader.searchComponenttraversal entirely. Thesepaths are unambiguously Java FQNs; the search would walk every
component mapping and find nothing. Only applies when no explicit
type:cfmlis requested.ClassUtil.checkPrimaryTypes— rewritten with length-based fastexits and
equalsIgnoreCaseinstead oftoLowerCase()+equals.Avoids allocating two intermediate strings per call for any className
that isn't a primary-type candidate.
ClassUtil.loadClass(ClassLoader, String)—new HashSet<Throwable>()deferred to the failure path. Cosmetic — C2 was already
escape-analysing this away.
Bug surfaced during the work
Clazz.getConstructor(args, convertArgument=true, convertComparsion=true)mutates the input
args[]array in place during the convertComparsionloop. The method only returns the resolved
Constructor, butConstructorInstance.invoke()callsconstr.newInstance(args)afterthe mutation, relying on the side effect for type coercion. Caching
the resolved Constructor bypasses the mutation; the 1-arg cache works
around this by passing a copy during the lookup and converting args
explicitly per call from the cached
paramTypes[0].Throughput change (2M iterations, JIT-warm, OpenJDK 21)
createObject( "java", X )createObject( "java", X ).init()new java.lang.StringBuilder()new java.lang.StringBuilder( 16 )Per-call cost: ~1.32μs → ~56–60ns on the non-init paths. Whole bench
wallclock: 62s → 3s.
Risk notes for review
jakarta.*short-circuit in_CreateComponent.isJavaFqncouldcollide with a user-mapped CFC in
jakarta.*. Thejava.*/javax.*/
sun.*/jdk.*/com.sun.*entries are JDK-internal and risk-free.Happy to drop the wider entries or gate behind a sysprop if the
collision risk is worth eliminating.
JavaProxy.CLASS_CACHEholds a strong reference to the ClassLoader asthe outer key. Cleared via
ConfigImpl.clearRPCClassLoader()pathsindirectly (new classloader → new cache entry; old loader remains
cached until invalidated). Production may want explicit eviction.
args[0].getClass()only. Forclasses with multiple overloads at the same arity where rating
tiebreaks decide, the cached Constructor could pick the wrong one
on a tied rating. Bench's
StringBuilder(int)vsStringBuilder(CharSequence)vsStringBuilder(String)with arg typeLongresolves deterministically; edge cases with two equally-ratedcandidates would need additional logic (probably caching
nullforambiguous and falling back).
Reflector.convertcall site inClazz.getMethodcached-dispatch (lines ~365–376) still uses try/catch. Not on the
constructor hot path; safe to leave for follow-up.
Test plan
mvn test(full suite, ~4:37 min): 0 failures, 0 errors,BUILD SUCCESS.
test.functions.createObject— 33 tests passedin 24.5s.
createObject/newbenchmark (abovethroughput table)
UDF→FunctionalInterface proxies, ObjectWrap unwrapping, Pojo
conversions, primitive-vs-reference paths through
_convertOrSentinel