Skip to content

Use memory.copy for the memmove QCall on browser-wasm - #136013

Merged
lewing merged 4 commits into
mainfrom
lewing-jiterp-vs-r2r-investigation
Oct 18, 2026
Merged

lewing merged 4 commits into
mainfrom
lewing-jiterp-vs-r2r-investigation

Conversation

@lewing

@lewing lewing commented Oct 17, 2026

Copy link
Copy Markdown
Member

The emscripten libc (6.0.2 as pinned here) implements memmove with musl's memmove.c: non-overlapping copies are forwarded to memcpy (which uses memory.copy for n >= 512), but overlapping copies always run a scalar loop, regardless of -mbulk-memory. SpanHelpers.Memmove sends overlapping buffers to the memmove QCall, which was bound directly to that libc symbol, so overlapping Span<T>.CopyTo/Array.Copy on CoreCLR wasm was far slower than on the Mono interpreter (whose jiterpreter emits memory.copy). In the Mono interpreter vs CoreCLR composite R2R comparison, Span<Char>.CopyToOverlap(Size: 3000) was ~20x and Span<Byte>.CopyToOverlap(Size: 3000) ~13.6x slower on CoreCLR.

This binds the browser memmove QCall to a small wrapper around __builtin_memmove, which lowers to memory.copy with -mbulk-memory. The wrapper has a TODO to remove it once the emsdk includes emscripten-core/emscripten#27654 (memmove via __builtin_wasm_memory_copy, in emscripten 6.0.10+); the bump is tracked in #136012. wasi-sdk 34.0 is not affected (its memmove already has a memory.copy path), so the change is browser-only.

Measurements: composite R2R browser-wasm publish (PublishReadyToRun=true), Release, run on node, overlapping byte CopyTo (dest = src + 1), ns per call:

Size Before After
1024 292 29
3000 782 44
6000 1520 74
12000 3030 136

The non-overlapping control is unchanged (~43 ns at 3000, ~125 ns at 6000). The benchmark is the Span<T>.CopyToOverlap shape from dotnet/performance. These were measured on a single arm64 macOS machine and I did not run Mono on the same machine.

Validated by building clr.native and clr+libs+packs for browser-wasm Release (confirming the object contains memory.copy) and running the R2R publish benchmark above. I did not run the library or runtime test suites, and did not build or test WASI.

Related to #136012

Note

This PR description was generated with AI assistance (GitHub Copilot) on behalf of @lewing.

The emscripten libc memmove copies overlapping buffers with a scalar loop,
so overlapping Span.CopyTo/Array.Copy on CoreCLR wasm was far slower than
the Mono interpreter, whose jiterpreter emits memory.copy. Bind the
memmove QCall to a wrapper around __builtin_memmove, which lowers to
memory.copy with -mbulk-memory.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0e8824e1-da24-4744-9a83-25d751374928
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.

@lewing lewing added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Oct 17, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

@jkotas

jkotas commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Are the limits at

private const nuint MemmoveNativeThreshold = 2048;
#endif
private const nuint ZeroMemoryNativeThreshold = 1024;
right for wasm? I guess the break-even point will be lower for wasm.

lewing and others added 2 commits October 17, 2026 20:45
The pinned emscripten libc memset is musl's scalar loop. Bind the memset
QCall to a __builtin_memset wrapper, which lowers to memory.fill with
-mbulk-memory. Clears of 1-4 KB get 1.4-1.7x faster.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0e8824e1-da24-4744-9a83-25d751374928
On browser-wasm R2R, the memmove QCall to memory.copy breaks even with the
managed copy loop at about 1024 bytes, and is 13-25% faster for 1-2 KB.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0e8824e1-da24-4744-9a83-25d751374928
@lewing

lewing commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

Are the limits at ... right for wasm? I guess the break-even point will be lower for wasm.

Lower, but not by much. Measured on browser-wasm R2R (V8, arm64 macOS) in one binary; overlapping copies always take the QCall, so they give the native cost at every size (ns, managed vs memory.copy): 128 B 8 vs 18, 512 B 14.5 vs 22, 1024 B 24 vs 25, 1536 B 34 vs 30, 2048 B 44 vs 34. Break-even is ~1024; the QCall's ~10 ns fixed cost keeps it from going lower.

System.Memory.Tests pass on browser composite R2R with these changes (52,254 run, 0 failed).

Note

This comment was generated with AI assistance (GitHub Copilot) on behalf of @lewing.

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0e8824e1-da24-4744-9a83-25d751374928
@lewing
lewing requested a review from jkotas October 18, 2026 22:12
@lewing
lewing enabled auto-merge (squash) October 18, 2026 22:12
@radekdoulik

Copy link
Copy Markdown
Member

/cc @BrzVlad

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-VM-coreclr os-browser Browser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants