alexcrichton edited issue #13386:
In https://github.com/bytecodealliance/wasmtime/pull/13382 I'm applying an optimization where
array.fillfori8-element arrays to be optimized to amemseton the host. This is relatively easy to do becausememory.fillalready has the infrastructure for this on the host andarray.fillis just reusing it. The intended benefit of this is that we get to use the host's vectorized routines forarray.fillas opposed to a per-byte-loop within CLIF. This benefit, however, is also theoretically applicable for elements of other sizes (e.g. all the way up to 128-bits). Implementing this, however, would require new libcalls on the host, for examplememory.fill{16,32,64,128}.This is doable without too too much effort, but this was left out of #13382 because it's not clear whether this is worth it. It'd likely be useful to investigate sibling peer compilers to see what they do in the face of
array.fillor similar for larger-than-8-bit-types.
alexcrichton commented on issue #13386:
I ended up doing a bit more work on https://github.com/bytecodealliance/wasmtime/pull/13382 for some more optimizations here. Specifically during
array.fill, andarray.new_defaultwhich uses the same internals, in addition to handlingi8arrays there's a check to see if the initialization value is a constant, and if that constant is a memset-able constant. For examplei64.const 0is a memset-able constant, as well asi64.const -1, buti64.const 1is not. That enables more usage ofmemset, notably witharray.new_default, which I think is going to be important.What this does not handle, however, is a few situations:
- We should still vectorize initialization of
i64.const 1in theory, the original premise of this issue- Due to codegen initializing
ref.i31 (i32.const -1), which has the bit-patterni32.const -1in CLIF, is not recognized and is not optimized tomemset. I think that's due to the fact that the shape at codegen-time is not a constant initialization value but probably instead either the result if aniaddorborinstruction. This is later const-prop'd to a constant, so the final IR looks like it should bememset, but the order of operations didn't go well.- While unrelated to
array.fill, the implementation ofarray.copydoes not use thememory.copylibcall forVMGcRef-based types. This can be used, however, when a GC implementation doesn't have read/write barriers (e.g. the null/copying collectors). In this situation we should ideally make a dynamic deduction based on the collector at compile time and usememory.copy's libcall unconditionally for all types if barriers aren't needed.
ttraenkler commented on issue #13386:
I hit the
array.copyslow path on v45.0.0 and found this issue as a follow-up to #13382 (which optimizedarray.fill, i8 →memset). Did the fill optimization also cover thearray.copypath, or is that the part still tracked here? A measured data point:On the default (drc) collector, i8
array.copyis ~4,100× slower than linearmemory.copy, and ~47× slower than even a hand-written element loop. Throughput on a 16 MiB i8 array (array.new_default×2, copydst[0..] ← src[0..]):
copy wasmtime 45.0.0 Node/V8 (25.x) array.copy(i8 GC)9 MiB/s 26,464 MiB/s manual element loop ( array.get_u/array.set)421 MiB/s 1,256 MiB/s linear memory.copy37,035 MiB/s 36,452 MiB/s In comparison V8's
array.copyruns at 26,464 MiB/s, ~2,900× faster than wasmtime's 9 MiB/s, and within ~1.4× of its ownmemory.copy(36,452 MiB/s). I am aware V8 has more resources so I only provide this as a reference point for orientation. The element-loop vsarray.copyinversion is the surprising part: it makes the native bulk op a pessimization, so I currently work around it by hand-rolling the copy. That linearmemory.copyis fast in wasmtime too suggests this is specific to the GCarray.copylowering rather than bulk-copy in general — consistent with the "optimizearray.copyfor more collectors" in the title.Setup: aarch64; default (drc) collector. wasmtime
45.0.0(377cd917a); Node25.x(V8 WasmGC). N = 16 MiB.Method: each
run_*export doesalloc+ R rounds in one call. For the wasmtime numbers I time the process and difference two round counts (so fixed startup/compile/alloc overhead cancels), exceptmemory.copywhich I measure absolutely (it's too fast for the difference to be stable). Node times the per-op loop directly withperformance.now(). The element-loop andarray.copyfigures jitter by ~±20% run-to-run, but the orderingarray.copy≪ element-loop ≪memory.copyis invariant. The module below is the exact one I measured — it assembles cleanly and reproduces these numbers.Minimal self-contained repro (drive with
wasmtime run -W gc=y,function-references=y --invoke run_arraycopy module.wasm 16777216 <rounds>, and likewiserun_elemloop/run_memcopy):(module (type $bytes (array (mut i8))) (memory 1024) (global $src (mut (ref null $bytes)) (ref.null $bytes)) (global $dst (mut (ref null $bytes)) (ref.null $bytes)) (global $n (mut i32) (i32.const 0)) (func $alloc (param $sz i32) (global.set $n (local.get $sz)) (global.set $src (array.new_default $bytes (local.get $sz))) (global.set $dst (array.new_default $bytes (local.get $sz)))) (func $arraycopy (param $r i32) (loop $l (array.copy $bytes $bytes (ref.as_non_null (global.get $dst)) (i32.const 0) (ref.as_non_null (global.get $src)) (i32.const 0) (global.get $n)) (local.set $r (i32.sub (local.get $r) (i32.const 1))) (br_if $l (local.get $r)))) (func $elemloop (param $r i32) (local $i i32) (loop $l (local.set $i (i32.const 0)) (block $d (loop $c (br_if $d (i32.ge_u (local.get $i) (global.get $n))) (array.set $bytes (ref.as_non_null (global.get $dst)) (local.get $i) (array.get_u $bytes (ref.as_non_null (global.get $src)) (local.get $i))) (local.set $i (i32.add (local.get $i) (i32.const 1))) (br $c))) (local.set $r (i32.sub (local.get $r) (i32.const 1))) (br_if $l (local.get $r)))) (func $memcopy (param $r i32) (loop $l (memory.copy (global.get $n) (i32.const 0) (global.get $n)) (local.set $r (i32.sub (local.get $r) (i32.const 1))) (br_if $l (local.get $r)))) (func (export "run_arraycopy") (param $sz i32) (param $r i32) (call $alloc (local.get $sz)) (call $arraycopy (local.get $r))) (func (export "run_elemloop") (param $sz i32) (param $r i32) (call $alloc (local.get $sz)) (call $elemloop (local.get $r))) (func (export "run_memcopy") (param $sz i32) (param $r i32) (call $alloc (local.get $sz)) (call $memcopy (local.get $r))))(N ≤ 32 MiB keeps
src+dstwithin the module's 64 MiB linear memory. Happy to provide more numbers, test other collectors, or try a patch if that helps.)
alexcrichton commented on issue #13386:
@ttraenkler all the optimizations and work on various bulk-transfer instructions will first be released with Wasmtime 46.0.0, so Wasmtime 45.0.0 is expected to have performance issues. This specific issue is about the follow-up to
array.fillwherememsetcan't be used but we could still do something more vectorized on the host.The
array.copypath is as optimal as known to be at this point (more-or-less). There's no further tracking issues there. You'll need to test something more recent than Wasmtime 45, however, to see that.You have a question about
array.copy, but did you have questions about anything else? I don't have a whole lot more to offer here other than "try main" unfortunately
ttraenkler commented on issue #13386:
@ttraenkler all the optimizations and work on various bulk-transfer instructions will first be released with Wasmtime 46.0.0, so Wasmtime 45.0.0 is expected to have performance issues. This specific issue is about the follow-up to
array.fillwherememsetcan't be used but we could still do something more vectorized on the host.The
array.copypath is as optimal as known to be at this point (more-or-less). There's no further tracking issues there. You'll need to test something more recent than Wasmtime 45, however, to see that.You have a question about
array.copy, but did you have questions about anything else? I don't have a whole lot more to offer here other than "try main" unfortunately@alexcrichton Thanks, will confirm on main if it can be reproduced. Just wasn't sure if #13382 entailed this fix.
If you're asking in general, as an author of a compiler prototype depending on Wasm GC the signal I read from #13216 is that GC will soon be considered ready for production. Reading more into the V8 numbers of my benchmark I wonder GC ops can close the perf gap to linear and this is just a matter of more optimizations or if GC ops are at a disadvantage by design.
ttraenkler edited a comment on issue #13386:
@ttraenkler all the optimizations and work on various bulk-transfer instructions will first be released with Wasmtime 46.0.0, so Wasmtime 45.0.0 is expected to have performance issues. This specific issue is about the follow-up to
array.fillwherememsetcan't be used but we could still do something more vectorized on the host.The
array.copypath is as optimal as known to be at this point (more-or-less). There's no further tracking issues there. You'll need to test something more recent than Wasmtime 45, however, to see that.You have a question about
array.copy, but did you have questions about anything else? I don't have a whole lot more to offer here other than "try main" unfortunately@alexcrichton Thanks, will confirm on main if it can be reproduced. Just wasn't sure if #13382 entailed this fix.
If you're asking in general, as an author of a compiler prototype depending on Wasm GC the signal I read from #13216 is that GC will soon be considered ready for production. Reading more into the V8 numbers of my benchmark I wonder if GC ops can close the perf gap to linear and this is just a matter of more optimizations or if GC ops are at a disadvantage by design.
alexcrichton commented on issue #13386:
Ah yeah #13382 definitely didn't fix this issue, this issue was spawned from discussion on that PR.
For more generally turning GC on-by-default I'd recommend leaving any thoughts over on #13216 (or filing specific issues). My personal rule of thumb is that a 10x-or-more slowdown relative to other engines is likely a bug in Wasmtime, a 5x-10x slowdown is probably a bug but maybe more difficult to fix, and a 1x-5x slowdown is often "v8 has man-decades of GC effort and Wasmtime has man-months of GC effort" although sometimes these issues are just bugs in Wasmtime as well. Feel free to open issues for anything you find, and if they're reproducible we can help diagnose and classify what's up.
ttraenkler commented on issue #13386:
Thanks, will do. My last point was not V8 vs. Wasmtime - but in general why even on V8 a significant gap remains between array.copy vs. memory.copy and if GC hits a theoretical limit. I will ask on the V8 repo and let you know if learn something worth sharing.
ttraenkler commented on issue #13386:
The results from my test on main confirm this issue indeed is already fixed and doesn't need to be tracked here:
size copy wasmtime 45.0.0 wasmtime main/dev 16 MiB array.copy10 MiB/s 30,710 MiB/s 16 MiB elem-loop 435 MiB/s 571 MiB/s 16 MiB memory.copy47,059 MiB/s 47,654 MiB/s Good to know this will land in 46!
Last updated: Jul 29 2026 at 05:03 UTC