You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Unobservable vec, and arrays writes always always emit for size >= 3 #96497
While revealing that dead Vec<_> writes always emit, to someone else, I decided to look into this for arrays.
It seems that, for any [T; N], such that T is not a ZST or enum with a single possible value, and 3 <= N, then array writes are always emit. This applies to allowing emitting redundant memcpys, memclrs, memfills, and all related operations.
I believe we should be adding a lifetime.end marker at the end of the function.
(Note that byval also fixes this, but it should be avoided because it changes the ABI in a way that limits optimization potential, by requiring the argument to be in a specific stack position.)
For Vec<T>, LLVM should be able to see that the stores are dead because it knows that __rust_dealloc frees memory.
It appears to be failing here because we conditionally call __rust_dealloc, and LLVM doesn't have enough information to optimize out the condition, so __rust_dealloc doesn't actually postdominate the store.
This is doing something like x.len() * size_of::<u32>() == 0, to decide whether or not to call __rust_dealloc. This check shouldn't be necessary, because we just checked x.len() == 0 above that, and we know the multiplication never wraps (but LLVM doesn't).
The store is optimized out for Box<[u8]>, because size_of::<u8> == 1 and the two conditions are trivially equivalent.
Despite this, it's still not optimized out for Vec<u8>, because again __rust_dealloc doesn't postdominate the store. In this case it seems like it's because the bounds check is based on x.len() and the dealloc check is based on x.capacity(), and LLVM doesn't know that x.len() <= x.capacity().
Yeah, that would likely work, or equivalently std::intrinsics::assume(x.len() <= x.capacity()). Might be worth opening a PR to run perf and make sure it's not too disruptive (assumes can interfere with other optimizations).
This is doing something like x.len() * size_of::<u32>() == 0, to decide whether or not to call __rust_dealloc. This check shouldn't be necessary, because we just checked x.len() == 0 above that, and we know the multiplication never wraps (but LLVM doesn't).
I'm working on this, should be straightforward to use unchecked multiplication in that size calculation
However this is a bit less important now than when this issue was first opened, since MIR DSE is now able to remove dead stores in some cases, including the simple motivating case for [T; N]: https://rust.godbolt.org/z/MGe59WrxY.
#121298 adds writable / dead_on_unwind (discussed above as noreadonunwind) to sret arguments.
But in order to improve this issue (i.e. to remove the stores to %x here: https://rust.godbolt.org/z/zexzc8oqa), we would also require dead_on_return. (Or on the rustc side we need to copy indirectly passed by-value arguments to an alloca at the entry of the function, but that might introduce other optimization problems.)
Activity
erikdesjardins commentedon Apr 29, 2022
For
[T; N], this is because we don't add lifetime markers to by-move arguments passed indirectly: https://rust.godbolt.org/z/vjd78n38a.I believe we should be adding a
lifetime.endmarker at the end of the function.(Note that
byvalalso fixes this, but it should be avoided because it changes the ABI in a way that limits optimization potential, by requiring the argument to be in a specific stack position.)For
Vec<T>, LLVM should be able to see that the stores are dead because it knows that__rust_deallocfrees memory.It appears to be failing here because we conditionally call
__rust_dealloc, and LLVM doesn't have enough information to optimize out the condition, so__rust_deallocdoesn't actually postdominate the store.For the simplified case of
Box<[T]>, see https://rust.godbolt.org/z/vnPM5o1K4. Note thatBox<[u32]>has an extra:This is doing something like
x.len() * size_of::<u32>() == 0, to decide whether or not to call__rust_dealloc. This check shouldn't be necessary, because we just checkedx.len() == 0above that, and we know the multiplication never wraps (but LLVM doesn't).The store is optimized out for
Box<[u8]>, becausesize_of::<u8> == 1and the two conditions are trivially equivalent.Despite this, it's still not optimized out for
Vec<u8>, because again__rust_deallocdoesn't postdominate the store. In this case it seems like it's because the bounds check is based onx.len()and the dealloc check is based onx.capacity(), and LLVM doesn't know thatx.len() <= x.capacity().erikdesjardins commentedon Jun 13, 2022
Yeah, that would likely work, or equivalently
std::intrinsics::assume(x.len() <= x.capacity()). Might be worth opening a PR to run perf and make sure it's not too disruptive (assumes can interfere with other optimizations).erikdesjardins commentedon Jun 13, 2022
I'm working on this, should be straightforward to use unchecked multiplication in that size calculation
Rollup merge of rust-lang#98078 - erikdesjardins:uncheckedsize, r=pet…
Rollup merge of rust-lang#98078 - erikdesjardins:uncheckedsize, r=pet…
erikdesjardins commentedon Jun 17, 2022
For the case of
[T; N], from #98121 it seems that emittinglifetime.endis not viable.A more viable option would be to add an attribute to upstream LLVM, like
noreadonunwind/noreadonreturndiscussed here: https://groups.google.com/g/llvm-dev/c/i0Z1FC51KVI.However this is a bit less important now than when this issue was first opened, since MIR DSE is now able to remove dead stores in some cases, including the simple motivating case for
[T; N]: https://rust.godbolt.org/z/MGe59WrxY.This is still a problem when the argument is directly overwritten though (https://rust.godbolt.org/z/G3odYbq78), e.g.
erikdesjardins commentedon Feb 24, 2024
#121298 adds
writable/dead_on_unwind(discussed above asnoreadonunwind) tosretarguments.But in order to improve this issue (i.e. to remove the stores to
%xhere: https://rust.godbolt.org/z/zexzc8oqa), we would also requiredead_on_return. (Or on the rustc side we need to copy indirectly passed by-value arguments to an alloca at the entry of the function, but that might introduce other optimization problems.)Auto merge of #145093 - nikic:dead-on-return, r=nnethercote
Auto merge of #145093 - nikic:dead-on-return, r=nnethercote
Auto merge of #145093 - nikic:dead-on-return, r=nnethercote
Auto merge of #145093 - nikic:dead-on-return, r=nnethercote
Auto merge of #145093 - nikic:dead-on-return, r=nnethercote
Auto merge of #145093 - nikic:dead-on-return, r=nnethercote
Auto merge of #145093 - nikic:dead-on-return, r=nnethercote
Auto merge of #145093 - nikic:dead-on-return, r=nnethercote
Auto merge of rust-lang#145093 - nikic:dead-on-return, r=nnethercote
Auto merge of rust-lang#145093 - nikic:dead-on-return, r=nnethercote
Auto merge of #145093 - nikic:dead-on-return, r=nnethercote
Auto merge of #145093 - nikic:dead-on-return, r=nnethercote
Auto merge of #145093 - nikic:dead-on-return, r=nnethercote
Auto merge of #145093 - nikic:dead-on-return, r=nnethercote
Auto merge of rust-lang#145093 - nikic:dead-on-return, r=nnethercote
Auto merge of rust-lang#145093 - nikic:dead-on-return, r=nnethercote