Summary
Improve JVM compiler/runtime specialization for hot closure and bit-packed numeric loops. Both workloads are dominated by generic dynamic-call and scalar machinery rather than their useful work.
Reproductions
perl dev/bench/benchmark_closure.pl
./jperl dev/bench/benchmark_closure.pl
perl examples/life_bitpacked.pl -r none
./jperl examples/life_bitpacked.pl -r none
Observed closure result (Apple Silicon development machine): system Perl completes 5,000 timethis iterations in 9.20 CPU seconds (543.48/s); PerlOnJava requires 44.35 CPU seconds (112.73/s), roughly 4.8x slower. A separate JFR run reported 46.42 CPU seconds (107.71/s).
life_bitpacked.pl (100x100, 5,000 generations, display disabled) was approximately 2.2–2.4x slower than system Perl in contemporaneous runs: system Perl 3.06–4.03 CPU seconds, PerlOnJava 10.98–14.01 CPU seconds. Wall-clock values vary with host load, so CPU time and sampled hotspots are the primary evidence.
JFR evidence
The closure benchmark's dominant samples are generic subroutine-call scaffolding:
| Samples |
Method |
| 2,354 |
RuntimeCode.apply(String, RuntimeArray, int) |
| 2,213 |
RuntimeCode.apply(RuntimeScalar, String, RuntimeBase[], int) |
| 1,886 |
generated closure body |
| 808 |
PerlRuntime.current/currentOrNull |
| 670 |
ThreadLocal.get / ThreadLocalMap.getEntry |
| 370 |
RuntimeCode.restoreCallerWarningScope |
| 213 |
RuntimeCode.enterCalleeWarningScope |
Each $f->() therefore pays for argument-frame installation, warning-scope lookup/push/pop, active-code/call-depth bookkeeping, closure-frame setup, and RuntimeList scalar-result coercion. The benchmark's arithmetic is not the limiting cost.
For Life, the original profile sampled BigInteger.and, BigInteger.valueOf, BitwiseOperators.unsignedValue, and BitwiseOperators.unsignedResult(BigInteger). WIP PR #1195 adds native-word handling for unsigned values that already fit in 64 bits; the post-change profile no longer samples those BigInteger operations. It still samples RuntimeCode.apply, RuntimeList/RuntimeScalar allocation, and closure-frame/argument machinery, so that local optimization does not close the end-to-end gap.
Scope and constraints
The call path implements observable Perl behavior. A fast path must preserve or safely decline when a callee/caller depends on:
@_, caller, or call-context inspection;
- dynamic warning scopes, lexical hints, or
local $^W;
eval, debugger hooks, overload, ties, or non-local control flow;
- closure-frame lifetime/protection and returned lvalues.
An attempted representation-only optimization that retained unsigned 64-bit values in a custom Number instead of BigInteger failed integer_pragma_word_arithmetic.t, zzzz_iv64_unsigned_bitwise.t, and math_bigint_method_chain.t; unsigned representation semantics must remain intact.
Proposed direction
- Add compiler analysis/annotations for statically safe zero-argument closure calls and simple scalar returns.
- Emit a guarded direct closure invocation path that bypasses generic
RuntimeCode.apply setup only when those annotations and runtime identity checks permit it; retain the existing path as fallback.
- Avoid
RuntimeArray/RuntimeList materialization and repeated warning-scope lookup for that path.
- Add focused project-owned regression/performance coverage for a captured-lexical zero-arg coderef call, including JVM/interpreter parity and system-Perl behavior validation.
- Continue scalar/integer specialization in Life only where it does not alter unsigned IV/BigInt semantics.
Related work
Summary
Improve JVM compiler/runtime specialization for hot closure and bit-packed numeric loops. Both workloads are dominated by generic dynamic-call and scalar machinery rather than their useful work.
Reproductions
Observed closure result (Apple Silicon development machine): system Perl completes 5,000
timethisiterations in 9.20 CPU seconds (543.48/s); PerlOnJava requires 44.35 CPU seconds (112.73/s), roughly 4.8x slower. A separate JFR run reported 46.42 CPU seconds (107.71/s).life_bitpacked.pl(100x100, 5,000 generations, display disabled) was approximately 2.2–2.4x slower than system Perl in contemporaneous runs: system Perl 3.06–4.03 CPU seconds, PerlOnJava 10.98–14.01 CPU seconds. Wall-clock values vary with host load, so CPU time and sampled hotspots are the primary evidence.JFR evidence
The closure benchmark's dominant samples are generic subroutine-call scaffolding:
RuntimeCode.apply(String, RuntimeArray, int)RuntimeCode.apply(RuntimeScalar, String, RuntimeBase[], int)PerlRuntime.current/currentOrNullThreadLocal.get/ThreadLocalMap.getEntryRuntimeCode.restoreCallerWarningScopeRuntimeCode.enterCalleeWarningScopeEach
$f->()therefore pays for argument-frame installation, warning-scope lookup/push/pop, active-code/call-depth bookkeeping, closure-frame setup, andRuntimeListscalar-result coercion. The benchmark's arithmetic is not the limiting cost.For Life, the original profile sampled
BigInteger.and,BigInteger.valueOf,BitwiseOperators.unsignedValue, andBitwiseOperators.unsignedResult(BigInteger). WIP PR #1195 adds native-word handling for unsigned values that already fit in 64 bits; the post-change profile no longer samples thoseBigIntegeroperations. It still samplesRuntimeCode.apply,RuntimeList/RuntimeScalarallocation, and closure-frame/argument machinery, so that local optimization does not close the end-to-end gap.Scope and constraints
The call path implements observable Perl behavior. A fast path must preserve or safely decline when a callee/caller depends on:
@_,caller, or call-context inspection;local $^W;eval, debugger hooks, overload, ties, or non-local control flow;An attempted representation-only optimization that retained unsigned 64-bit values in a custom
Numberinstead ofBigIntegerfailedinteger_pragma_word_arithmetic.t,zzzz_iv64_unsigned_bitwise.t, andmath_bigint_method_chain.t; unsigned representation semantics must remain intact.Proposed direction
RuntimeCode.applysetup only when those annotations and runtime identity checks permit it; retain the existing path as fallback.RuntimeArray/RuntimeListmaterialization and repeated warning-scope lookup for that path.Related work
dev/bench/benchmark_closure.plexamples/life_bitpacked.pl