Module org.elasticsearch.compute
Class ConstantMethodResultSpecializer
java.lang.Object
org.elasticsearch.compute.operator.ConstantMethodResultSpecializer
Specializes a class at runtime that extends a given evaluator base class and supplies
specified constants in a way HotSpot's C2 compiler treats as JIT-time constants.
For primitives the constant is baked as a
static final field on the
generated subclass. For reference types the constant is passed via class data
and loaded through a condy bootstrap into the accessor method. Either
way, once the subclass is loaded, C2 inlines the accessor through the monomorphic
call site and folds the constant — unlocking optimizations such as
Granlund-Montgomery strength reduction for integer divide, devirtualization of
subsequent method calls on a reference receiver, branch elimination, and so on.
The factory used by ESQL's @Fixed(jitConstant = true) codegen calls
one of the *ConstantSubclass methods. Authors do not invoke this class
directly.
Cache design — weak refs + admission filter
Two structures handle the workload-shape tradeoffs:- Class cache:
ConcurrentHashMap<Key, WeakReference<Class>>. Unbounded; JVM manages size via reachability. A constant-specialized class is kept alive by any live evaluator instance referencing it. Once nothing references the class, GC reclaims it and the weak ref clears — the next access starts fresh. - Admission tracker: bounded LRU counters keyed by specializer key (default 4096
entries, ~128 KB). Specialization triggers only when a key's count reaches
admissionThreshold(default 2). Single-occurrence keys never trigger a spin — the caller falls back to the non-folded path. This eliminates the spin tax for one-off cold keys.
Properties this gives:
- High-cardinality one-off workload (e.g. 50 K distinct constants none repeating): zero spins. Per-access overhead = one CHM lookup + one counter increment (~50 ns).
- Bursty workload: hot keys spin once on second access; stay alive while bursts run (live instances pin them); GC after a quiet period.
- Steady-state hot: spin once per key, then constant hit rate forever.
- Stampede: concurrent threads requesting the same missing key all funnel through
a single
computeIfAbsent— only one spin happens.
Cost calibration
Measured on Apple aarch64 JDK 21 after JIT warmup:- Specialization cost: mean 13 μs, p99 52 μs, max 1.5 ms (cold-JIT outlier)
- Per-class metaspace: ~1.4-2 KB
- Cache hit lookup: ~50 ns
- Admission-rejected path: ~50 ns (one CHM lookup + counter inc)
Measurement is mandatory for adoption
Adoption decisions are not predictable from code reading alone — both wins and regressions surface that the static rules don't catch. Every flag adoption must come with a JMH bench case (const-folded + variable baseline) measured on at least three microarchitectures. SeeFixed.jitConstant()
for the decision framework and the calibration table from PR #148678 covering
11 attempted adoptions, of which 4 shipped, 1 was kept noise-band-neutral,
and 6 were reverted with measurement evidence.-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final ConstantMethodResultSpecializerJVM-wide default specializer instance. -
Method Summary
Modifier and TypeMethodDescriptionvoidStress-test / tooling support: clear all caches and counters and reset config.voidsetAdmissionThreshold(int newThreshold) Set admission threshold.specializeDouble(Class<T> baseClass, String methodName, double value) Same asspecializeLong(java.lang.Class<T>, java.lang.String, long)but fordouble.specializeInt(Class<T> baseClass, String methodName, int value) Same asspecializeLong(java.lang.Class<T>, java.lang.String, long)but forint.specializeLong(Class<T> baseClass, String methodName, long value) Materialize (or fetch from cache) a hidden subclass ofbaseClassthat overridesmethodName()(which the base must declare asprotected abstract long methodName()) to returnvalueas a JIT-time constant.specializeReference(Class<T> baseClass, String methodName, Class<V> valueType, V value) Same asspecializeLong(java.lang.Class<T>, java.lang.String, long)but for an arbitrary reference type.
-
Field Details
-
SHARED
JVM-wide default specializer instance. Generated evaluator factories callConstantMethodResultSpecializer.SHARED.specializeLong(...); tests construct their own instance vianew ConstantMethodResultSpecializer()for isolation (no shared state).The cache + admission tracker are intentionally process-global by default: two instances would each spin a hidden class for the same
(base, accessor, value)triple, duplicating JIT compilations and defeating the cache. Usenewonly when you want isolated state.
-
-
Method Details
-
specializeLong
public <T> Optional<Class<? extends T>> specializeLong(Class<T> baseClass, String methodName, long value) Materialize (or fetch from cache) a hidden subclass ofbaseClassthat overridesmethodName()(which the base must declare asprotected abstract long methodName()) to returnvalueas a JIT-time constant. ReturnsOptional.empty()only when the cache is at capacity and adding this entry would require eviction — the caller should fall back to the non-folded path in that case. -
specializeInt
public <T> Optional<Class<? extends T>> specializeInt(Class<T> baseClass, String methodName, int value) Same asspecializeLong(java.lang.Class<T>, java.lang.String, long)but forint. -
specializeDouble
public <T> Optional<Class<? extends T>> specializeDouble(Class<T> baseClass, String methodName, double value) Same asspecializeLong(java.lang.Class<T>, java.lang.String, long)but fordouble. -
specializeReference
public <T,V> Optional<Class<? extends T>> specializeReference(Class<T> baseClass, String methodName, Class<V> valueType, V value) Same asspecializeLong(java.lang.Class<T>, java.lang.String, long)but for an arbitrary reference type. The constant is passed via class data and loaded through acondybootstrap referencingMethodHandles.classData(MethodHandles.Lookup, String, Class). -
setAdmissionThreshold
public void setAdmissionThreshold(int newThreshold) Set admission threshold. A key must be seen this many times before a class is specialized for it. Default = 2 (skip the first-time access — usually a one-off). Set to 1 to disable admission filtering (every miss spins immediately, like the prior behavior).Public so cross-package tests in esql can force the Standard / constant-specialized paths by setting the threshold extreme. No production caller; if a tuning API becomes useful, route it through
SHAREDexplicitly. -
resetForTest
public void resetForTest()Stress-test / tooling support: clear all caches and counters and reset config. Not for production use. Marked public so external benchmark/stress harnesses can reset state between scenarios; production code should never call this.
-