perf: lazy range arrays for O(1) std.range creation#771
Merged
stephenamar-db merged 1 commit intodatabricks:masterfrom Apr 12, 2026
Merged
perf: lazy range arrays for O(1) std.range creation#771stephenamar-db merged 1 commit intodatabricks:masterfrom
stephenamar-db merged 1 commit intodatabricks:masterfrom
Conversation
Replace eager Array[Eval] allocation in std.range with a lazy representation that stores only the start value and length. Elements are computed on demand via Val.cachedNum, making std.range(1, 1000000) O(1) instead of O(n). Key changes: - Add _isRange boolean flag and _rangeFrom field to Val.Arr - Val.Arr.range() factory creates lazy ranges without allocation - value(i)/eval(i) compute range elements on the fly - reversed() creates reversed ranges without materialization - materializeRange() converts to flat array when bulk access needed - Double-reverse correctly restores original range direction The comparison benchmark (std.range(1,1M) + [1] < std.range(1,1M) + [2]) improves from 16.2ms to 0.028ms (579x faster) on JMH, and sjsonnet native is now 2x faster than jrsonnet on this benchmark. Inspired by jrsonnet's RangeArray (arr/spec.rs).
stephenamar-db
approved these changes
Apr 12, 2026
Contributor
Author
|
I think I want to introduce a RangeArr, which will reduce 2 fields for the common Arr, and will submit it later. |
He-Pin
added a commit
to He-Pin/sjsonnet
that referenced
this pull request
Apr 12, 2026
Move lazy range state (_isRange, _rangeFrom) from inline fields in Arr to a dedicated RangeArr subclass. This saves ~9 bytes per non-range Arr instance (boolean + int + alignment padding), benefiting the common case where arrays are not created via std.range. Key changes: - Arr is no longer final; RangeArr extends Arr with range-specific fields - arr and _length visibility widened to private[Val] for subclass access - isConcatView made final for Scala 2.x @inline compatibility - Range-specific branches removed from Arr.value/eval/asLazyArray/reversed - RangeArr overrides value/eval/asLazyArray/reversed with range logic - Arr.range() factory now returns RangeArr instances Upstream: follow-up to databricks#771 (lazy range arrays)
This was referenced Apr 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
std.range(from, to)eagerly allocates anArray[Eval]ofVal.Numobjects. For large ranges likestd.range(1, 1000000), this creates 1M objects upfront even when only a subset of elements are ever accessed. This is particularly wasteful in patterns like array comparison where two large ranges share a common prefix and only the tail elements differ.Key Design Decision
Encode lazy range state directly in
Val.Arrusing a boolean_isRangeflag and_rangeFrominteger field. WhenisRangeis true,arris null and elements are computed on demand viaVal.cachedNum(pos, _rangeFrom + i). This avoids introducing a new subclass and keeps the hot-pathvalue(i)dispatch to a single boolean check.A separate
_isRangeboolean flag is used instead of a sentinel value (e.g.,Int.MinValue) to avoid collisions with valid range start values.Reversed ranges store the high end as
_rangeFromand count down (_rangeFrom - i), with correct double-reverse handling that restores the original forward range.Modification
Val.scala: Add_isRangeboolean +_rangeFromfield toVal.Arr. Updatevalue(i),eval(i),asLazyArray,reversed()to handle range state. AddmaterializeRange()for bulk access. AddArr.range()factory.ArrayModule.scala: Replace eagerArray[Eval]allocation instd.rangewithVal.Arr.range(pos, from, size)for O(1) creation.lazy_range_correctness.jsonnet— covers iteration, concat+comparison, slicing, sort, reverse, double-reverse, member, map, foldl, empty ranges, large ranges.Benchmark Results
JMH (JVM, Scala 3.3.7)
Hyperfine (Scala Native vs jrsonnet)
Previous: comparison was 1.36x slower than jrsonnet → Now 2.07x faster 🎉
Analysis
The
comparisonbenchmark (std.range(1, 1000000) + [1] < std.range(1, 1000000) + [2]) previously allocated 2MVal.Numobjects upfront, then compared element by element. With lazy ranges:ConcatViewwraps range + singleton without copyingsharedConcatPrefixLengthdetects shared range prefix (same object reference), skips to the differing tail elementThis turns an O(n) operation into O(1) end-to-end.
No regressions detected across all 35+ JMH benchmarks and full test suite (420 tests × 3 Scala versions × 4 platforms).
References
The same optimization lives in jrsonnet and scala to too.
Result
All tests pass (
./mill __.test). Comparison benchmark flipped from 1.36x slower to 2.07x faster than jrsonnet.