Stream: git-wasmtime

Topic: wasmtime / issue #13386 Optimize `array.fill` with more l...


view this post on Zulip Wasmtime GitHub notifications bot (May 15 2026 at 16:58):

alexcrichton edited issue #13386:

In https://github.com/bytecodealliance/wasmtime/pull/13382 I'm applying an optimization where array.fill for i8-element arrays to be optimized to a memset on the host. This is relatively easy to do because memory.fill already has the infrastructure for this on the host and array.fill is just reusing it. The intended benefit of this is that we get to use the host's vectorized routines for array.fill as 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 example memory.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.fill or similar for larger-than-8-bit-types.

view this post on Zulip Wasmtime GitHub notifications bot (May 15 2026 at 16:59):

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, and array.new_default which uses the same internals, in addition to handling i8 arrays there's a check to see if the initialization value is a constant, and if that constant is a memset-able constant. For example i64.const 0 is a memset-able constant, as well as i64.const -1, but i64.const 1 is not. That enables more usage of memset, notably with array.new_default, which I think is going to be important.

What this does not handle, however, is a few situations:

view this post on Zulip Wasmtime GitHub notifications bot (Jun 05 2026 at 07:07):

ttraenkler commented on issue #13386:

I hit the array.copy slow path on v45.0.0 and found this issue as a follow-up to #13382 (which optimized array.fill, i8 → memset). Did the fill optimization also cover the array.copy path, or is that the part still tracked here? A measured data point:

On the default (drc) collector, i8 array.copy is ~4,100× slower than linear memory.copy, and ~47× slower than even a hand-written element loop. Throughput on a 16 MiB i8 array (array.new_default ×2, copy dst[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.copy 37,035 MiB/s 36,452 MiB/s

In comparison V8's array.copy runs at 26,464 MiB/s, ~2,900× faster than wasmtime's 9 MiB/s, and within ~1.4× of its own memory.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 vs array.copy inversion is the surprising part: it makes the native bulk op a pessimization, so I currently work around it by hand-rolling the copy. That linear memory.copy is fast in wasmtime too suggests this is specific to the GC array.copy lowering rather than bulk-copy in general — consistent with the "optimize array.copy for more collectors" in the title.

Setup: aarch64; default (drc) collector. wasmtime 45.0.0 (377cd917a); Node 25.x (V8 WasmGC). N = 16 MiB.

Method: each run_* export does alloc + 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), except memory.copy which I measure absolutely (it's too fast for the difference to be stable). Node times the per-op loop directly with performance.now(). The element-loop and array.copy figures jitter by ~±20% run-to-run, but the ordering array.copy ≪ element-loop ≪ memory.copy is 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 likewise run_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+dst within the module's 64 MiB linear memory. Happy to provide more numbers, test other collectors, or try a patch if that helps.)

view this post on Zulip Wasmtime GitHub notifications bot (Jun 05 2026 at 17:27):

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.fill where memset can't be used but we could still do something more vectorized on the host.

The array.copy path 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

view this post on Zulip Wasmtime GitHub notifications bot (Jun 05 2026 at 20:52):

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.fill where memset can't be used but we could still do something more vectorized on the host.

The array.copy path 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.

view this post on Zulip Wasmtime GitHub notifications bot (Jun 05 2026 at 20:52):

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.fill where memset can't be used but we could still do something more vectorized on the host.

The array.copy path 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.

view this post on Zulip Wasmtime GitHub notifications bot (Jun 05 2026 at 21:05):

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.

view this post on Zulip Wasmtime GitHub notifications bot (Jun 05 2026 at 21:38):

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.

view this post on Zulip Wasmtime GitHub notifications bot (Jun 06 2026 at 06:47):

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.copy 10 MiB/s 30,710 MiB/s
16 MiB elem-loop 435 MiB/s 571 MiB/s
16 MiB memory.copy 47,059 MiB/s 47,654 MiB/s

Good to know this will land in 46!


Last updated: Jul 29 2026 at 05:03 UTC