Commit 661b8b2
authored
perf: lazy range arrays for O(1) std.range creation (#771)
## Motivation
`std.range(from, to)` eagerly allocates an `Array[Eval]` of `Val.Num`
objects. For large ranges like `std.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.Arr` using a boolean `_isRange`
flag and `_rangeFrom` integer field. When `isRange` is true, `arr` is
null and elements are computed on demand via `Val.cachedNum(pos,
_rangeFrom + i)`. This avoids introducing a new subclass and keeps the
hot-path `value(i)` dispatch to a single boolean check.
A separate `_isRange` boolean 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 `_rangeFrom` and count down
(`_rangeFrom - i`), with correct double-reverse handling that restores
the original forward range.
## Modification
- **`Val.scala`**: Add `_isRange` boolean + `_rangeFrom` field to
`Val.Arr`. Update `value(i)`, `eval(i)`, `asLazyArray`, `reversed()` to
handle range state. Add `materializeRange()` for bulk access. Add
`Arr.range()` factory.
- **`ArrayModule.scala`**: Replace eager `Array[Eval]` allocation in
`std.range` with `Val.Arr.range(pos, from, size)` for O(1) creation.
- **New test**: `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)
| Benchmark | Before (ms/op) | After (ms/op) | Change |
|-----------|----------------|---------------|--------|
| comparison | 16.204 | 0.028 | **579x faster** 🔥 |
| reverse | 7.033 | 6.823 | 3% faster ✅ |
| bench.06 | 0.226 | 0.218 | 4% faster ✅ |
| setUnion | 0.638 | 0.623 | No regression ✅ |
| gen_big_object | 1.122 | 0.973 | No regression ✅ |
| All others | — | — | No regression ✅ |
### Hyperfine (Scala Native vs jrsonnet)
| Benchmark | sjsonnet (ms) | jrsonnet (ms) | Ratio |
|-----------|--------------|---------------|-------|
| comparison | 6.5 ± 1.4 | 13.4 ± 1.9 | **sjsonnet 2.07x faster** ✅ |
| reverse | — | — | ~1.04x faster (no regression) |
| bench.06 | ~6.8 | ~5.3 | jrsonnet 1.29x (startup-dominated, unchanged)
|
**Previous**: comparison was 1.36x slower than jrsonnet → **Now 2.07x
faster** 🎉
## Analysis
The `comparison` benchmark (`std.range(1, 1000000) + [1] < std.range(1,
1000000) + [2]`) previously allocated 2M `Val.Num` objects upfront, then
compared element by element. With lazy ranges:
1. **O(1) range creation** — only stores start + length, no allocation
2. **O(1) concat** — `ConcatView` wraps range + singleton without
copying
3. **O(1) comparison** — `sharedConcatPrefixLength` detects shared range
prefix (same object reference), skips to the differing tail element
This 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.1 parent ef04cb4 commit 661b8b2
4 files changed
Lines changed: 137 additions & 13 deletions
File tree
- sjsonnet
- src/sjsonnet
- stdlib
- test/resources/new_test_suite
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
342 | 342 | | |
343 | 343 | | |
344 | 344 | | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
345 | 355 | | |
| 356 | + | |
346 | 357 | | |
347 | 358 | | |
348 | 359 | | |
| |||
352 | 363 | | |
353 | 364 | | |
354 | 365 | | |
355 | | - | |
| 366 | + | |
356 | 367 | | |
357 | 368 | | |
358 | 369 | | |
| |||
362 | 373 | | |
363 | 374 | | |
364 | 375 | | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
365 | 380 | | |
366 | 381 | | |
367 | 382 | | |
| |||
378 | 393 | | |
379 | 394 | | |
380 | 395 | | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
381 | 399 | | |
382 | 400 | | |
383 | 401 | | |
| |||
403 | 421 | | |
404 | 422 | | |
405 | 423 | | |
| 424 | + | |
406 | 425 | | |
407 | 426 | | |
408 | 427 | | |
| |||
447 | 466 | | |
448 | 467 | | |
449 | 468 | | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
450 | 489 | | |
451 | 490 | | |
452 | 491 | | |
| |||
513 | 552 | | |
514 | 553 | | |
515 | 554 | | |
516 | | - | |
517 | | - | |
518 | | - | |
519 | | - | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
520 | 584 | | |
521 | 585 | | |
522 | 586 | | |
523 | 587 | | |
524 | 588 | | |
525 | 589 | | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
526 | 605 | | |
527 | 606 | | |
528 | 607 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
292 | 292 | | |
293 | 293 | | |
294 | 294 | | |
295 | | - | |
296 | | - | |
297 | | - | |
298 | | - | |
299 | | - | |
300 | | - | |
301 | | - | |
302 | | - | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
303 | 300 | | |
304 | 301 | | |
305 | 302 | | |
| |||
Lines changed: 47 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
0 commit comments