Skip to content

Optimize TypedArray bulk operations with Span/Array.Copy#2488

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:byte-array-optimization
May 31, 2026
Merged

Optimize TypedArray bulk operations with Span/Array.Copy#2488
lahma merged 1 commit into
sebastienros:mainfrom
lahma:byte-array-optimization

Conversation

@lahma

@lahma lahma commented May 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

TypedArrays are commonly used in high-performance scenarios, but several %TypedArray%.prototype methods copied/scanned the backing buffer one byte (or one element) at a time, routing each step through GetValueFromBuffer/SetValueInBuffer (a TypedArrayValue decode/encode plus an Array.Copy(elementSize)). This started from a review finding that the per-iteration constraint counters in the set/slice byte-copy loops were correct but masked the real win: replacing the byte-by-byte copy with a single bulk System.Array.Copy/Span operation, which also moves the constraint check out of the hot loop.

The backing store is a plain byte[] (JsArrayBuffer._arrayBufferData), so System.Array.Copy — already the established bulk-copy primitive in this codebase (CloneArrayBuffer, Resize, ArrayBufferPrototype.Slice) and documented to handle overlapping regions with memmove semantics — applies directly.

Changes

  • set / slice / copyWithin / with — same-element-type transfers now use a single System.Array.Copy. set also skips the now-redundant same-buffer clone for the same-type path (memmove gives the bit-identical result without the allocation).
  • fill — materialize the fill value once, then replicate its byte pattern across the range via exponential doubling (endian-agnostic, all TFMs).
  • reverse / toReversed — reverse element order directly in the buffer using the vectorized Span<T>.Reverse (size-dispatched; endian-agnostic since whole element blocks are swapped by position).
  • indexOf / lastIndexOf / includes — for integer element types, when the search value is exactly representable and the platform is little-endian, scan with the vectorized Span<T>.IndexOf/LastIndexOf. All other cases fall back to the existing element loop.
  • Construction from a CLR array (FillTypedArrayInstance<T>) — bulk MemoryMarshal copy instead of a per-element Convert.ToDouble (which boxed every element).

Correctness notes

  • Spec gap fixed: set/copyWithin on a view over an immutable ArrayBuffer now throw TypeError. The direct SetValueInBuffer byte path bypassed AssertNotImmutable (only the o[i] = value indexer enforced it).
  • slice aliasing: when a @@species constructor returns a target that aliases the source buffer, slice keeps a forward byte-by-byte copy (the spec-mandated overlap "smear"), since memmove would differ there.
  • Search coercion side-effects: the fast paths fall back to the generic per-element loop if fromIndex/searchElement coercion detached or shrank the buffer.

Verification

  • test262: 99,015 passed, 0 failures / 0 regressions (full suite); 4,314 TypedArray tests green, including the immutable-ArrayBuffer, @@species-aliasing, and resizable-buffer-during-coercion cases.
  • Unit tests: full Jint.Tests suite green on net10.0 and net472.
  • Build: 0 warnings (warnings-as-errors) across net462;netstandard2.0;netstandard2.1;net8.0;net10.0.

Benchmarks

New TypedArrayBulkBenchmark (10,000-element arrays, BenchmarkDotNet ShortRun, AMD Ryzen 9 5950X, .NET 10). Before = base of this branch, after = this PR, same benchmark in both runs:

Method Before After Speedup Allocated (before → after)
set 364.6 µs 1.03 µs ~354× 736 B → 736 B
set (self-overlapping) 315.5 µs 1.83 µs ~172× 41.8 KB → 1.6 KB
slice 315.6 µs 2.09 µs ~151× 41 KB → 41 KB
copyWithin 300.2 µs 0.98 µs ~307× 688 B → 688 B
fill 112.5 µs 1.33 µs ~85× 320 KB → 744 B
reverse 285.2 µs 0.91 µs ~315× 320 KB → 648 B
toReversed 332.5 µs 2.55 µs ~130× 361 KB → 41 KB
with 283.2 µs 1.89 µs ~150× 361 KB → 41 KB
indexOf (miss) 166.5 µs 0.75 µs ~222× 680 B → 680 B
includes (miss) 116.4 µs 0.83 µs ~140× 680 B → 680 B
construct from CLR array 106.7 µs 1.33 µs ~80× 600 KB → 40 KB
construct from JS TypedArray 1.57 µs 1.59 µs — (unchanged; already bulk-copied) 41 KB → 41 KB

🤖 Generated with Claude Code

Replace per-byte/per-element loops in TypedArray methods with bulk memory
operations, addressing a review finding that the byte-by-byte copy loops in
set/slice were the real bottleneck (not the per-iteration constraint counters).

- set/slice/copyWithin/with: same-type transfers use System.Array.Copy
  (memmove); set skips the now-redundant clone for the same-type path
- fill: materialize the value once, replicate via exponential doubling
- reverse/toReversed: vectorized Span<T>.Reverse over the backing buffer
- indexOf/lastIndexOf/includes: vectorized Span<T>.IndexOf/LastIndexOf for
  integer element types (exactly-representable search values, little-endian)
- constructor from a CLR array: bulk MemoryMarshal copy instead of per-element
  Convert.ToDouble boxing

Also closes a pre-existing spec gap: set/copyWithin on a view over an immutable
ArrayBuffer now throw TypeError (the direct SetValueInBuffer path bypassed
AssertNotImmutable).

slice keeps a forward byte-by-byte copy when a @@species target aliases the
source buffer (spec-mandated overlap "smear"); the search fast paths fall back
to the generic per-element loop when fromIndex/searchElement coercion detaches
or shrinks the buffer.

99,015 test262 pass, 0 regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lahma
lahma merged commit 31e859d into sebastienros:main May 31, 2026
4 checks passed
@lahma
lahma deleted the byte-array-optimization branch May 31, 2026 13:53
@sebastienros

Copy link
Copy Markdown
Owner

Can you do the same on my bank account?

@lahma

lahma commented May 31, 2026

Copy link
Copy Markdown
Collaborator Author

Can you do the same on my bank account?

Nice example of spec vs optimal.

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.

2 participants