Skip to content

perf: single-field object inline storage to avoid LinkedHashMap allocation#687

Open
He-Pin wants to merge 1 commit intodatabricks:masterfrom
He-Pin:perf/single-field-object
Open

perf: single-field object inline storage to avoid LinkedHashMap allocation#687
He-Pin wants to merge 1 commit intodatabricks:masterfrom
He-Pin:perf/single-field-object

Conversation

@He-Pin
Copy link
Copy Markdown
Contributor

@He-Pin He-Pin commented Apr 5, 2026

Motivation

Jsonnet frequently creates objects with a single field, e.g., { n: X } in patterns like Fib { n: X }. Currently ALL objects allocate a java.util.LinkedHashMap to store their fields, which is expensive for single-field objects due to LinkedHashMap's overhead (Node array, load factor, entry objects).

This optimization stores the single field key and member inline in Val.Obj using two simple fields, completely avoiding the LinkedHashMap allocation for the common single-field case.

Key Design Decisions

  1. Inline storage: Two new constructor params singleFieldKey: String and singleFieldMember: Obj.Member store the field directly when there is exactly one field.

  2. Lazy LinkedHashMap construction: When the full map is needed (e.g., key iteration, addSuper), getValue0 lazily constructs a LinkedHashMap from the inline storage. This avoids paying the cost unless actually needed.

  3. Lazy builder allocation in visitMemberList: The LinkedHashMap builder is only allocated when the second field is encountered, not upfront. For single-field objects, no LinkedHashMap is ever allocated during construction.

  4. Fast paths for common operations: valueRaw, containsKey, and hasKeys check singleFieldKey directly before falling through to the LinkedHashMap path, avoiding materialization.

Modification

Val.scala

  • Added singleFieldKey/singleFieldMember constructor params to Val.Obj
  • getValue0: Added branch to lazily construct LinkedHashMap from inline storage
  • valueRaw: Added single-field fast path using String.equals instead of HashMap.get
  • hasKeys: Returns true immediately for single-field objects
  • containsKey: Checks singleFieldKey.equals(k) for non-super single-field objects
  • containsVisibleKey, allKeyNames, visibleKeyNames: Changed from direct value0 to getValue0

Evaluator.scala

  • visitMemberList: Lazy builder allocation — tracks singleKey/singleMember/fieldCount during field iteration. When exactly 1 field, creates Val.Obj with inline storage. Builder LinkedHashMap is only allocated on the 1→2 field transition.

Benchmark Results

JMH (JVM, Scala 3.3.7)

Benchmark Before (ms/op) After (ms/op) Change
bench.02 48.296 39.419 -18.4%
bench.04 33.261 31.466 -5.4%
comparison 23.093 21.956 -4.9%
realistic2 68.960 69.609 ~0% (noise)
reverse 10.685 10.301 -3.6%

No regressions across all 35 benchmarks.

Hyperfine (Scala Native, macOS ARM64)

Benchmark master This PR Change
bench.02 69.1 ms 67.8 ms -1.9%
bench.04 511 ms 529 ms ~0% (noise)

Native improvement is modest — the JVM's JIT compiler inlines the singleFieldKey != null branch more aggressively than ahead-of-time compilation.

Analysis

The -18.4% improvement on bench.02 is expected: this benchmark (recursive Fibonacci with object inheritance) creates millions of single-field objects like Fib { n: X }. Each now avoids a LinkedHashMap allocation, and the valueRaw fast path bypasses the HashMap lookup entirely.

bench.04 and comparison also benefit because they create objects in tight loops. The optimization is transparent — single-field objects behave identically to multi-field objects, with lazy LinkedHashMap construction on demand.

References

  • Upstream jit branch: d284ecf4 (single-field object avoid LinkedHashMap)

Result

  • ✅ All 140 tests pass
  • ✅ Zero benchmark regressions across 35 JMH benchmarks
  • ✅ bench.02: -18.4% JMH (recursive Fibonacci with object inheritance)
  • ✅ bench.04: -5.4% JMH, comparison: -4.9% JMH

…ation

For objects with exactly one field (common in patterns like `{ n: X }`),
store the field key and member inline in Val.Obj instead of allocating a
LinkedHashMap. The LinkedHashMap is lazily constructed only when needed
(e.g., key iteration via getAllKeys).

Key changes:
- Val.Obj: added singleFieldKey/singleFieldMember constructor params
- getValue0: lazily constructs LinkedHashMap from inline storage
- valueRaw: single-field fast path with String.equals instead of HashMap.get
- hasKeys/containsKey: fast paths to avoid forcing LinkedHashMap materialization
- visitMemberList: lazy builder allocation, only for 2+ field objects

Upstream: jit branch d284ecf (single-field object avoid LinkedHashMap)
@He-Pin He-Pin marked this pull request as ready for review April 5, 2026 10:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant