Skip to content

Extend zero-copy string slice views to bounded-waste substrings#2518

Merged
lahma merged 2 commits into
sebastienros:mainfrom
lahma:perf/string-substring-zero-copy
Jun 9, 2026
Merged

Extend zero-copy string slice views to bounded-waste substrings#2518
lahma merged 2 commits into
sebastienros:mainfrom
lahma:perf/string-substring-zero-copy

Conversation

@lahma

@lahma lahma commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

What

String.prototype.substring(start, end) clamps a negative end to 0 and then swaps so the larger index is the end. So str.substring(12000, -1) on a 131072-char string yields a 12000-char result whose pinned backing-string remainder (119072 chars) exceeded JsString.CreateSliced's zero-copy retention guard (length*2 >= source.Length → 24000 < 131072), forcing a full string.Substring copy on every call. The visually-identical str.slice(12000, -1) resolves -1 relative to length without swapping → a 119071-char span → passes the guard → zero-copy view. That asymmetry is why slice was ~1% of dromaeo-object-string's allocation while substring was 89.85% (116 MB/iter).

This adds an absolute-waste cap to the guard: a slice also becomes a SlicedString view when the bytes it pins but never uses (source.Length - length) stay within SliceViewMaxWastedChars (128K chars). So a moderate slice of a large source avoids the per-call copy while retention stays bounded (≤ result + 256 KB). Small slices still copy.

The PR also corrects StringSliceBenchmarks.SubstringLargeDiscard, which used substring(12000, str.length - 1) (positive args, no swap → already a view) and so never measured the dromaeo shape that copies — the reason #2506's benchmark reported substring as a small fraction.

Benchmarks (default job, stash A/B)

Benchmark Baseline This PR Δ
SubstringLargeDiscard — Allocated 117,776 KB 550 KB −99.5%
SubstringLargeDiscard — time 6.39 ms 1.96 ms −69%
SliceSmall (must still copy) 628.57 KB 628.57 KB flat
SliceThenRead / SliceLargeDiscard flat

End-to-end dromaeo-object-string allocation 129 MB → 14.6 MB (−88.7%) (substring 116 MB → 1.6 MB), via MemoryProbe. Jint stays rank #1 on object-string by time.

Tests

  • Jint.Tests net10 (3067) + net472 (3005)
  • Jint.Tests.PublicInterface net10 (79) + net472 (79)
  • Test262: 99260 passed, 0 failed (substring/substr/slice suites + the indexOf/startsWith/split/equality consumers that operate on the returned view)

🤖 Generated with Claude Code

String.prototype.substring(start, end) clamps a negative end to 0 and swaps,
so substring(12000, -1) on a 131072-char string yields a 12000-char result
whose pinned backing-string remainder (119072 chars) exceeded the
JsString.CreateSliced retention guard (length*2 >= source.Length), forcing a
full copy on every call — 116 MB/iter in dromaeo-object-string (89.85% of its
allocation), while the equivalent slice(12000, -1) returned a zero-copy view.

Add an absolute-waste cap to the guard: a slice also becomes a view when the
pinned-but-unused remainder (source.Length - length) stays within
SliceViewMaxWastedChars (128K chars), so a moderate slice of a large source
avoids the per-call copy without unbounded retention. Small slices still copy.

Also corrected StringSliceBenchmarks.SubstringLargeDiscard, which used
substring(12000, str.length-1) (positive args, no swap → already a view) and
therefore never exercised the dromaeo shape.

SubstringLargeDiscard (default job, stash A/B): -99.5% allocated
(117776 -> 550 KB), -69% time; SliceSmall/SliceThenRead/SliceLargeDiscard
guards flat. End-to-end dromaeo-object-string 129 -> 14.6 MB (-88.7%).
Jint.Tests net10 (3067) + net472 (3005), PublicInterface (79/79),
Test262 99260/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lahma
lahma enabled auto-merge (squash) June 9, 2026 14:58
@lahma
lahma merged commit 0ba359f into sebastienros:main Jun 9, 2026
7 of 8 checks passed
@lahma
lahma deleted the perf/string-substring-zero-copy branch June 9, 2026 15:42
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