Optimize the implementation of memory.fill#13367
Conversation
This commit is a refactoring and reimplementation of the `memory.fill` instruction in WebAssembly to be more optimal. Previously the implementation was a libcall with the raw arguments to the instruction which had various amounts of overhead in the host to implement the semantics of the instruction. The implementation now performs a bounds-check inline in wasm itself and while there's still an unconditional libcall it's a much simpler libcall. The goal of this commit is to spearhead the shape of this optimization to pave the way for other libcalls to get modified as well. In isolation `memory.fill` isn't too particularly interesting here. Eventually though I'd like to have `memory.copy` and `array.copy`, for example, bottom out in the same libcall which is a simple `memmove` or similar. For now `memory.fill` was simple enough so I've started here.
cfallin
left a comment
There was a problem hiding this comment.
Looks reasonable to me overall -- thanks!
I'm curious if you've measured overheads here; naively, I'd expect that having a libcall at all is the main fixed cost, and moving logic from the libcall to compiled Wasm code is at best neutral (we have to do the same checks either way). In my mind the real win will come when we can avoid a trampoline and the usual runtime entry machinery, instead directly calling memmove/memcpy/memset (our local version or the real thing). And, as long as we're avoiding relocations in cwasms, the most efficient thing would be a direct call to a little local routine we put alongside the trampolines; on x86-64 a rep stosb / rep movsb (because these are optimized in microcode to do whole-cache-line-sized things, i.e. they're as good as it gets), on aarch64 and others whatever a reasonably good memcpy/memmove/memset loop would be. What do you think?
| /// | ||
| /// Does not perform any in-bounds checks, so `val` must already be | ||
| /// validated to be in-bounds. | ||
| fn unchecked_cast_index_to_pointer( |
There was a problem hiding this comment.
cast_index_to_pointer reads a little ambiguously to me -- maybe cast_wasm_addr_ty_to_native_ptr_ty?
|
Agreed yeah, and the main goal here isn't performance of That being said, our safe VM machinery inside of Wasmtime isn't particularly optimized. A 1024-byte The profile before is: and the profile after is: where |
|
Benchmark in question is this, in which I golf |
This is a dual of bytecodealliance#13367 which updates the implementation of the `memory.copy` intrinsic. The same shape is applied here -- perform validation in compiled code and then delegate via a libcall to the actual host implementation. The intention is to subsume the `array_copy_non_gc_ref_elems` libcall into this one entirely in a subsequent commit.
* Optimize `array.fill` and `array.copy` This commit is a step towards optimizing the codegen and implementation of the `array.fill` and `array.copy` GC instructions. This builds on the intrinsics refactored in bytecodealliance#13367 and bytecodealliance#13368 to be used in the GC implementation as well. For example `array.fill` and `array.new_default` for `i8` arrays will now use `memset` to fill an array. The larger change here is the reimplementation of `array.copy` to inline the loop within generated CLIF. Prior to this commit the `array.copy` function was exclusively built as a libcall. There were two versions of this libcall -- one for GC references and one for non GC references. Even with this minor optimization, however, the benchmark in bytecodealliance#13279 is still quite slow. The main reason for this is that the GC references version of the libcall has an extremely high constant overhead per element copied. This is due to the nature of the implementation using completely safe APIs which engages a lot more dynamic type checks and GC heap tracking than necessary. The implementation of `array.copy` in this commit now looks like: * First the bounds of the copy operation are all validated. Traps happen if either array is out of bounds. * Next raw heap addresses are calculated for the start/end of the src/dst. * For copyable types (e.g. `i8`, `i32`, etc), the implementation then delegates to the same intrinsic as `memory.copy`, a host-defined `memmove`. * For GC types (e.g. `anyref`), the implementation instead has an inline loop which copies elements from the source to the destination. This copy is structured as two loops, either a forwards copy or a backwards copy, and only one is taken. The result of this implementation is that the benchmark in bytecodealliance#13279 is massively faster. What previously took minutes-to-hours now completes in seconds, even with the DRC collector. One caveat with this implementation is that when delegating to `memory.copy` or `memory.fill` there's an unnecessary-if-wasmtime-has-no-bugs bounds check double-check the host will not fault. While wasm code will gracefully catch segfaults the host will not, so this is necessary to ensure that in the face of heap corruption the host never faults. In the future it might make sense to split out functionality like this into separate helper CLIF functions to keep the source relatively lean. In the meantime though the massive performance benefits seem to outweigh the size costs (to me at least). * Fix conditional compile * Review comments * Specialize more bit patterns in `array.fill` * Fix 32-bit test
This commit is a refactoring and reimplementation of the
memory.fillinstruction in WebAssembly to be more optimal. Previously the implementation was a libcall with the raw arguments to the instruction which had various amounts of overhead in the host to implement the semantics of the instruction. The implementation now performs a bounds-check inline in wasm itself and while there's still an unconditional libcall it's a much simpler libcall.The goal of this commit is to spearhead the shape of this optimization to pave the way for other libcalls to get modified as well. In isolation
memory.fillisn't too particularly interesting here. Eventually though I'd like to havememory.copyandarray.copy, for example, bottom out in the same libcall which is a simplememmoveor similar. For nowmemory.fillwas simple enough so I've started here.