fix(redundant_clone): Make visit_local_usage analyse loop bodies instead of giving up on them - #17495
Conversation
|
Thanks for the pull request. A reviewer will take a look after it receives 2 community reviews. In the meantime, we would highly appreciate if you could try to review any of PRs waiting on community reviews. Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (
|
|
Lintcheck changes for 7724e6c
This comment will be updated if you push new changes |
| }; | ||
|
|
||
| traversal::Postorder::new(&mir.basic_blocks, location.block, None) | ||
| for tbb in traversal::Postorder::new(&mir.basic_blocks, location.block, None) |
There was a problem hiding this comment.
May be trivial for some, but in case it is not:
Blocks visited in reverse postorder, which is execution order.
Example A -> {B, C}, B -> D, C -> D (a diamond).
Callers take the first recorded location, so iterating blocks by index would be wrong.
| worklist.extend(successors(location.block, dead)); | ||
| } | ||
|
|
||
| while let Some((bb, dead)) = worklist.pop() { |
There was a problem hiding this comment.
I think pop order here is irelevant due to symetry.
Also could not find a case where the order is relevant for the linting decision.
Not sure, does not seem to matter. 🤷🏻♂️
There was a problem hiding this comment.
In general for fixpoint analysis it matters only for perf reasons. The ideal order for most forward analyses would be to always check blocks in a reverse-postorder manner. For this case the maximum distance to the top of the lattice is the number of locals (either one or two) so there's not any real risk of checking a block too many times.
|
#14599 has a few interesting testcases The cases which are incorrectly linting and #14599 does not lint are all unrelated to this change and already fail on main. testcases that I am ignoring becauese does not compile under current rust (not sure about polonius, have not tried)
I am not, because that would not compile with let mut x = black_box(String::new());
for _ in 0..10 {
let _y = x.clone(); //~ redundant_clone
black_box(&mut x);
}I am not, because that would not compile with let mut x = black_box(String::new());
for _ in 0..10 {
let y = black_box(String::new());
x = y.clone(); //~ redundant_clone
black_box(&y);
}let mut x = black_box(String::new());
for _ in 0..10 {
let y = black_box(String::new());
x = y.clone(); //~ redundant_clone
black_box(&y);
}
black_box(&x);🤔 The loop-cases which I AM missing (=not linting) are all closely related to aliasing let mut x = black_box(String::new());
for _ in 0..10 {
let y = x; //~ redundant_clone
black_box(y);
x = black_box(String::new());
}let mut x = black_box(String::new());
for _ in 0..10 {
let y = x.clone(); //~ redundant_clone
black_box(y);
x = black_box(String::new());
}let mut x = black_box(String::new());
let mut y = black_box(String::new());
for _ in 0..10 {
y = x.clone(); //~ redundant_clone
x = black_box(String::new());
}
black_box(&x);let mut x = black_box(String::new());
let mut y = x.clone(); //~ redundant_clone
for _ in 0..10 {
black_box(y);
x = black_box(String::new());
y = x.clone(); //~ redundant_clone
} |
|
seems that the aliasing lifeness issues are closely related, but would bloat the PR again, so lets likely better push that to a second PR. |
b89b547 to
0e3c647
Compare
|
Reminder, once the PR becomes ready for a review, use |
|
@rustbot ready |
| *state &= !ENQUEUED_FLAG; | ||
| } | ||
|
|
||
| const { assert!(N <= ENQUEUED_FLAG.trailing_zeros() as usize) } |
There was a problem hiding this comment.
Why is N limited to 7 here, from what I understand the old implementation has no restriction? I can't seem to find a reason looking at the code itself.
There was a problem hiding this comment.
N is currently well below this (1 or 2) as it is the size of locals.
Previous code did not have this limitation, but also did not use this and I am not sure what kind of lint would.. 7 is kind of a lot for this helper fn, given what locals is.
If we want more than 7 locals, we need to increase from an u8 bitset to something larger (u16/.../u128). Should be an easy fix, if we need to.
The assert just makes sure that if N > 7 that this is an error, given that this is likely 2-3 steps away from where N is actually specified via the size of locals.
Actually, my original impl was to use an DenseBitSet, but perf wise Jarcho is right, u8 is better..
See #17495 (comment) for context
There was a problem hiding this comment.
if you want to have a code reason why this is one of the invariants of this impl:
There was a problem hiding this comment.
If a caller ends up needing more than seven locals they shouldn't be using this function in the first place. The implementation does not scale well with the number of locals and fixing that requires preprocessing the body in a way that isn't worth doing when only checking one or two locals (which is all we need anyway).
fa164b8 to
6c55bee
Compare
|
This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
…stead of giving up on them
6c55bee to
7724e6c
Compare
|
@rustbot review |
View all comments
Fixes #16096
redundant_clonemissed everyclone()in a loop body:visit_local_usagereturnedNonewheneverlocation.blockwas in a cycle, andredundant_clonepasses the clone's own block.The
matchin the issue is a bit of a red-herring.The arm ending in
returnlinted only because it leaves the cycle.The bailout can't just be dropped, or
issue13900::regressionfalse-positives.In #13900
a's only read precedeslocationin the clone's own block, andV::visit_placehides it even though the cycle re-executes it.What separates the two cases is where the local is declared.
One declared inside the body is
StorageDead-ed before the back edge andStorageLive-d again next iteration, so it is a distinct variable each time round.One declared outside is not...
reachable_while_storage_livewalks forward fromlocation, stops a path once every tracked local isStorageDead, and returnsNoneonly iflocation.blockis reached again while one is still live.readonly_write_lockandused_exactly_oncepassSTART_BLOCK, where the old bailout was unreachable sincebb0is never a branch target, therefore I don't think this specific issue exists there.At least, I cannot come up with syntax that is not already caught by previous clippy.
changelog: [
redundant_clone]: detect redundant clones inside loop bodies without aliases