Skip to content

enhancement: specialize hot closure calls and bit-packed numeric loops #1196

Description

@fglock

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

  1. Add compiler analysis/annotations for statically safe zero-argument closure calls and simple scalar returns.
  2. 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.
  3. Avoid RuntimeArray/RuntimeList materialization and repeated warning-scope lookup for that path.
  4. Add focused project-owned regression/performance coverage for a captured-lexical zero-arg coderef call, including JVM/interpreter parity and system-Perl behavior validation.
  5. Continue scalar/integer specialization in Life only where it does not alter unsigned IV/BigInt semantics.

Related work

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:performancePerformance optimization and hangsenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions