[libc] Always use __builtin_wasm_memory_copy in memmove - #27654
Conversation
|
Where is the benchmark? |
cdc4e0d to
924ff71
Compare
AI-Generated: memmove Benchmark Details & WorkloadsHere is the microbenchmark used to measure Source Code (
|
924ff71 to
0ebd33c
Compare
|
Thanks! Interestingly I see musl is much faster when inlining is enabled, in fact, 1-2% faster than the builtin. I guess it can specialize the code for the constants in the benchmark. That does suggest to me that this is probably not much of a win for LTO, but still seems worthwhile for size. |
Ah, so these number should really include LTO I guess? But.. these files are explicitly excluded from LTO in because they are part of But..... I guess binaryen is doing inlining in |
Followup to #27653. Replace Musl's `memmove.c` and the `-Oz` manual loop in `emscripten_memmove.c` with `__builtin_wasm_memory_copy`. WebAssembly's `memory.copy` instruction is specified with overlapping copy (`memmove`) semantics, and engines (V8, SpiderMonkey, JSC) implement it using native host `memmove`. In Musl's C implementation, misaligned overlapping copies fall back to a byte-by-byte scalar loop in wasm, which is 15x to 33x slower than `memory.copy`. Overlapping copy benchmark results (200k iterations across sizes 1B to 16KB): Shift forward (dest > src): - V8: 34.51ms vs 576.50ms (94.0% faster, 16.7x) - SpiderMonkey: 33.73ms vs 1129.64ms (97.0% faster, 33.5x) - JavaScriptCore: 33.32ms vs 715.26ms (95.3% faster, 21.5x) Shift backward (dest < src): - V8: 34.28ms vs 551.30ms (93.8% faster, 16.1x) - SpiderMonkey: 33.74ms vs 546.25ms (93.8% faster, 16.2x) - JavaScriptCore: 32.90ms vs 535.86ms (93.9% faster, 16.3x)
|
Hmm, looks like when LLVM inlines here it ends up generating code with calls to |
|
I'll re-run the numbers with the memmove implementation in the separate non-LTO compilation unit (mimicking our libc setup). |
|
The benchmark above uses |
|
Oh, it does prevent inlining. I experimented with removing that, to see what the inlined result would be. I was assuming that would reflect the LTO case. But IIUC you said these files were not part of LTO? (why?) |
Certain symbols in compiler-rt / libc are considered "libcalls"s, which mean llvm can generate calls to them during LTO. Such functions cannot themselves be part of LTO. See the long comment in |
|
Did you find any cases there this change is not actually an improvement? even if inlining is enabled (or we make these function LTO) this change is never a regression is it? |
0ebd33c to
d18ee60
Compare
|
Given that LTO was not optimizing these, I see no regression. If LTO was doing so, this might have been a tiny 1-2% regression, as mentioned above. Really a shame LTO can't handle this kind of thing. It is exactly a great candidate for an LTO speedup... I guess this PR gets us 98-99% of that benefit without LTO though. |
|
Couldn't we optimize this in principle? We don't actually need to prevent LTO from inlining this, we just need to ensure that an implementation of memcpy is available at native link time in addition to whatever LTO might do. I get that linking is already complicated though. Another idea, I wonder if there's some way to improve the optimizer. If it can inline the other memcpy implementation and end up with better code, why isn't it doing that with this replacement? e.g., why is it seeing this implementation and not a memcpy intrinsic (which it would presumably understand better than this bare instruction)? I guess none of that matters if we're not letting LTO see the implementation in the first place. |
| #include "musl/src/string/memmove.c" | ||
| static void *__memmove(void *dest, const void *src, size_t n) { | ||
| // memory.copy traps on OOB zero-length copies, but memmove must not. | ||
| if (n) { |
There was a problem hiding this comment.
Is this check still needed? IIUC, this zero-length check should no longer be necessary after PR llvm/llvm-project#112617.
There was a problem hiding this comment.
My understanding is that __builtin_wasm_memory_copy is a lower level intrinsic that lowers to just the single wasm instruction.
See the comment in the referenced PR about // Use MEMCPYhere instead ofMEMORY_COPY..
One is __builtin_memcpy (with memcpy semantics) and other is __builtin_wasm_memory_copy (single wasm instruction).
Also IIRC, one cannot use __builtin_memcpy in the implementaion of the memcpy libcall because that will lead to an infinite self-call (since __builtin_memcpy is itself allowed to lower to memcpy).
I was testing the benchmark here, which put the memmove impl in the same file, as just some plain C code. The optimizer can inline that in the same compilation unit, then specialize for the specific sizes we are memmoving etc. But, in contrast, the intrinsic is "opaque" even if it is put inside this file. That is, it sees a call to an intrinsic and stops there. But, perhaps LLVM could actually see |
In theory I think believe the bug in question (references in the comment in |
Yeah I was thinking that most instances of memcpy should show up in the IR as llvm.memcpy (and not calls to this code) which can then be unrolled however the optimizer likes. It seems like it should be rare in practice that it should even make a difference. |
|
Ok that land this ? |
|
lgtm to land. If we can find a way to optimize further that sounds good, but it may really be just 1-2% on memmove operations, which is likely very small in real-world programs. |
Yes, in practice LLVM will already have unrolled/inlined/codegened calls to |
Followup to #27653.
Replace Musl's
memmove.cand the-Ozmanual loop inemscripten_memmove.cwith__builtin_wasm_memory_copy.WebAssembly's
memory.copyinstruction is specified with overlapping copy (memmove) semantics, and engines (V8, SpiderMonkey, JSC) implement it using native hostmemmove.In Musl's C implementation, misaligned overlapping copies fall back to a byte-by-byte scalar loop in wasm, which is 15x to 33x slower than
memory.copy.Overlapping copy benchmark results (200k iterations across sizes 1B to 16KB):
Shift forward (dest > src):
Shift backward (dest < src):