Re: version planning

Paul King <[email protected]> Tue, 7 Jul 2026 22:54:16 +1000
Newsgroups gmane.comp.lang.groovy.devel
Message-ID <CAMbkE7QggLPEvSPRr0RexGDZbiwrfxJUCV_SkgiUvRjCjqLeVA@mail.gmail.com>
Hi Jochen,

Response below.

Cheers, Paul.

--------

> This means asType adaption and explicitCastArguments does not cost? Also
> interesting would be the cost of inserting and dropping arguments in
> terms of Lambda forms

Measured (pure java.lang.invoke, no Groovy, fresh JVM per mode, count of
spun LambdaForm$* classes; 8 variations per mode, minus a raw-handle
control where applicable):

  asType / explicitCastArguments (ref or prim)   +0 over the raw handles
  insertArguments x8 (same shape)                +7 total (one-time, then f=
ree)
  guardWithTest x8 / catchException x8           +5 / +6 (same pattern)
  dropArguments, arities 1-8                     +26 (~3.2 per resulting sh=
ape)
  asCollector / asSpreader, arities 1-8          +34 / +24 (~3-4 per arity)
  8 raw handles, distinct primitive signatures   +36 (~4.5 per basic-type s=
hape)
  8 raw handles, distinct reference signatures   +8 (all erase to L)

So yes =E2=80=94 asType/explicitCastArguments literally cost nothing by
themselves. The law that explains all of it (and the earlier table you
weren't sure how to read): LambdaForm cost tracks DISTINCT BASIC-TYPE
SHAPES touched, not combinators and not call sites. Reference types all
erase to basic type L, so reference diversity is free; primitives and
arity are the basic-type dimensions, so they are the only real
multipliers. Your mismatch theory was right in effect but the mechanism
is shape-counting: the adaptation step is innocent, the primitive shapes
it connects are the cost. In our full chains one site-shape drags ~13-17
LFs because bootstrap collector + guards + spreader each derive shapes
from it.

Earlier-table reading guide: each row is one generated workload varying
one dimension, run in a fresh JVM; the number is loaded LambdaForm
classes minus the 142 a bare Groovy runtime loads. "32 sites vs 1 site:
+29 vs +29" means sites are free at constant shape.

> still possible that creating those hidden classes costs too much though.
> Also we need an efficient lookup for those [...]

The GROOVY-12137 spike suggests the template class may be optional
rather than required: plain Method.invoke (which shares the JDK's own
per-arity infrastructure post-JEP-416) already removes ~2/3 of the
per-shape LF cost and gives 1.34-1.40x faster cold dispatch on
many-cold-sites workloads, with zero per-method class creation and no
lookup structure at all. Promotion bounds reflective residence to at
most threshold calls per receiver shape, after which the hot path is
byte-for-byte what we have today. So the hidden-class template becomes a
targeted optimisation of cold per-call overhead, to be justified by
measurement, not a prerequisite =E2=80=94 and if we do build it, hanging th=
e
cache off CachedMethod (like the old callsite-constructor cache, so
classloader lifecycle comes for free) seems the natural lookup.

On Tue, Jul 7, 2026 at 9:48=E2=80=AFPM Jochen Theodorou <[email protected]>=
 wrote:
>
> On 7/7/26 11:48, Paul King wrote:
> [...]
> >> My idea would be to have a baseline benchmark in Groovy unrelated code
> >> in pure Java. And then adjust everything to be relative to that.
> >
> > Your mail crossed the implementation :-) That's exactly what landed as
> > GROOVY-12127 on July 5: pure-Java "calibration ruler" benchmarks (integ=
er
> > throughput, pointer-chase, allocation churn) now run inside every CI su=
ite
> > shard, and the per-PR summary comment shows a hardware-calibrated speed=
up
> > column next to the raw one, plus a warning when the runner deviates >15=
% from
> > the baseline hardware. Your caveat =E2=80=94 that a runner isn't unifor=
m *within* a
> > run either =E2=80=94 is fair; the planned refinement is a second ruler =
that sorts to
> > the end of each suite's execution so we can report intra-run drift too.
>
> nice
>
> [...]> Small generated workloads, each varying ONE dimension, delta
> LambdaForms vs a
> > 142-LF baseline JVM (deterministic, reproducible):
> >
> >    arity diversity (arities 1-8, all-Object args)     +136   (~17 LF/ar=
ity)
> >    primitive arg diversity (8 primitive shapes)       +110   (~13 LF/sh=
ape)
> >    one (R,int)Object call site (control)               +29
> >    primitive RETURN diversity (vs uniform int +14)     +27    (~2 LF ea=
ch)
> >    8 sites, all-Object args (control)                  +17
> >    reference arg diversity (8 distinct classes)        +16    (~free)
> >    reference return diversity                          +12    (~free)
> >    32 sites vs 1 site, identical shape+target        +29 vs +29  (0 per=
 site)
>
> interesting, though not sure I interpret the numbers correctly
>
> > Conclusions:
> >
> > 1. My W2 proposal (erase reference types in indy descriptors) is dead =
=E2=80=94 the
> >     JVM already shares LambdaForms across reference-typed shapes, so er=
asure
> >     would buy roughly nothing. Your skepticism was correct; no compiler=
 change
> >     should be written for that.
> >
> > 2. Your mismatch theory is confirmed, with a refinement: the callsite<-=
>target
> >     adaptation cost is real but specifically for the PRIMITIVE dimensio=
ns of
> >     MethodType (boxing adaptations and per-arity forms). Reference-type
> >     mismatches fold into shared forms for free. LambdaForms are shared =
across
> >     call sites of identical shape =E2=80=94 the cost is per SHAPE, not =
per site.
>
> per shape was clear to me. The Reference-type mismatch being essentially
> free but not primitives is kind of surprising. I mean it is clear to me
> that the primitives do cost, but reference types being free not so much.
> This means asType adaption and explicitCastArguments does not cost? Also
> interesting would be the cost of inserting and dropping arguments in
> terms of Lambda forms
>
> > 3. This strengthens the cold-tier direction you're already circling (th=
e
> >     reflection/MOP-dispatching cold path, your hidden-class template id=
ea): an
> >     (Object, Object[])Object cold dispatcher erases both expensive dime=
nsions
> >     at once =E2=80=94 one shape for every arity and every primitive pat=
tern =E2=80=94 which is
> >     precisely why reflection is cold-fast. The specialised primitive sh=
apes
> >     then get paid lazily, only by the sites that earn promotion to hot.
>
> still possible that creating those hidden classes costs too much though.
> Also we need an efficient lookup for those, since we would have one per
> "called" method in the receiver globally... well per module and
> classloader would also work for JPMS compatibility.
>
> bye Jochen
>