Repository navigation
Description
As part of #44619, one topic that keeps coming up is that we have to find some way to mitigate the risk of large, implicit copies. Indeed, this risk exists today even without any changes to the language:
let x = [0; 1024 * 10];
let y = x; // maybe you meant to copy 10K words, but maybe you didn't.In addition to performance hazards, implicit copies can have surprising semantics. For example, there are several iterator types that would implement Copy, but we were afraid that people would be surprised. Another, clearer example is a type like Cell<i32>, which could certainly be copy, but for this interaction:
let x = Cell::new(22);
let y = x;
x.set(23);
println!("{}", y.get()); // prints 22For a time before 1.0, we briefly considered introducing a new Pod trait that acted like Copy (memcpy is safe) but without the implicit copy semantics. At the time, @eddyb argued persuasively against this, basically saying (and rightly so) that this is more of a linting concern than anything else -- the implicit copies in the example above, after all, don't lead to any sort of unsoundness, they just may not be the semantics you expected.
Since then, a number of use cases have arisen where having some kind of warning against implicit, unexpected copies would be useful:
- Iterators implementing
Copy - Copy/clone closures (closures that are copy can be surprising just as iterators can be)
Cell,RefCell, and other types with interior mutability implementingCopy- Behavior of
#[derive(PartiallEq)]and friends with packed structs - A coercion from
&TtoT(i.e., it'd be nice to be able to dofoo(x)wherefoo: fn(u32)andx: &u32)
I will writeup a more specific proposal in the thread below.
Activity
nikomatsakis commentedon Nov 1, 2017
I think the lint would work roughly like this. We define a set of types that should not be implicitly copied. This would include any type whose size is >N bytes (where N is some arbitrary number, perhaps tunable via an attribute on the crate), but also any struct that is tagged with
#[must_clone]. The name of#[must_clone]is probably bad; it is meant to be reminiscent of#[must_use]-- basically it's saying that, to copy this type, one ought to clone it (e.g.,let y = x.clone()rather thanlet y = x), so that the semantics are clear to the reader.We would then comb code for implicit copies. This is (somewhat) easy to define at the MIR level: any use of a user-defined variable (not a temporary) that is of a type implementing
Copy. Here are some examples where this would trigger. Let's assumexis a variable of "must clone" type:One interesting case that would, by this definition, be linted against is this:
This one is interesting because, unlike the others, there is no way to readily silence the lint, except by adding an
#[allow(implicit_copy)]. That is usually a bad sign -- we may not want to lint on this case. One can argue, certainly, that themoveis sufficient to warn the reader. Not sure if I buy that. =)It is also interesting that the use cases here perhaps breakdown into two categories:
Cell<i32>, iterators, closures).In fact, I would argue that the latter is the majority case, so maybe we just want to separate this into two lints, or else say that structs with the
#[must_clone]tag only warn if -- after the implicit copy -- there is a subsequent use. This would actually solve themove || use(x)problem as well, since one could rewrite the closure expression to:eddyb commentedon Nov 1, 2017
I would prefer it if this was more specific, e.g.
forloops warning if the iteratee is used afterwards.arielb1 commentedon Nov 2, 2017
@nikomatsakis
You don't want to warn on any use - that would warn on moves too. You want to warn on uses where the variable is live after the use.
nikomatsakis commentedon Nov 8, 2017
@arielb1 indeed.
nikomatsakis commentedon Nov 8, 2017
@eddyb do you have a specific example of something you would want to warn, and something you would not want?
eddyb commentedon Nov 8, 2017
@nikomatsakis Copying a
Rangearound shouldn't warn (just like any other pair). Using aCopyvariable implementingIntoIterator/Iterator(e.g. aRange) after iterating on it with aforloop should warn, as previously discussed.nikomatsakis commentedon Jan 23, 2018
@eddyb it seems like we could generalize then to a lint that works like this:
#[must_clone]:CellPlaceX (of such a type) is copied and then used again later.#[must_clone]:IntoIterator::into_iterPlaceX and then X is used again later.Maybe a better name than
#[must_clone], especially for the second case.nikomatsakis commentedon Jan 23, 2018
Anyway, regardless of the nitty gritty details, let me leave a few pointers here for how we might implement this. @cramertj is hopefully gonna take a look.
First off, let's look a bit at MIR. MIR definition is here.. A crucial concept I referenced above is a
Place, which represents a path:rust/src/librustc/mir/mod.rs
Lines 1231 to 1243 in 4e3901d
The arguments to MIR statements are always operands, which explicitly identify a copy:
rust/src/librustc/mir/mod.rs
Lines 1399 to 1403 in 4e3901d
I think right now we will produce a
Copyoperand whenever the input is of copy type, and aMoveoperand whenever the input may not be ofCopytype. It is an error to use a value after it is moved. So our analysis would look forCopyoperands.One way to do that is with the MIR visitor, which lets you quickly walk the IR. In particular we'd probably want to override the
visit_operandmethod:rust/src/librustc/mir/visit.rs
Lines 142 to 146 in 4e3901d
There is code for computing liveness -- basically, whether a variable may be used again. That code lives in
liveness.rs. You can useliveness_of_localsto compute what is live and where:rust/src/librustc_mir/util/liveness.rs
Lines 117 to 120 in 4e3901d
but that only gives you values at basic block boundaries. You will want
simulate_blockto walk through and find out what is live at any particular point:rust/src/librustc_mir/util/liveness.rs
Lines 161 to 167 in 4e3901d
So I guess that roughly the idae would be to identify copies where the value's type is marked to be linted. We would then compute the liveness at the point of copy and see if the
Placebeing moved may be referenced again.Some annoying challenges:
a.band then usesa.c, we can't really tell thatStill, this is a lint after all, it doesn't have to be totally precise. I think that for the first version of the code I would only worry about local variables and ignore more complex
Placevalues. That seems like a simple thing to fix up later.nikomatsakis commentedon Feb 16, 2018
@qmx may take a look at this, since @cramertj never had time
5 remaining items
RalfJung commentedon Mar 28, 2018
This can all happen in one thread.
RalfJung commentedon Mar 28, 2018
hanna-kruppe commentedon Mar 28, 2018
Oh yeah you're right. The mention of another thread really mislead me there.
RalfJung commentedon Mar 28, 2018
Sorry for that, not sure what I thought. "Another function" is really what I meant (though it can be the same, as in my counterexample).
nikomatsakis commentedon Apr 3, 2018
@RalfJung in retrospect, the RefCell problem is obvious -- basically, of course copying is bad, for the same reason that
borrow()isn't infallible. Silly me.qmx commentedon Apr 20, 2018
Just found out about rust-lang/rfcs#2407 - that would be a nice suggestion for the linter in the future, wouldn't it?
est31 commentedon Feb 27, 2021
Just as a data point, this issue affects real world programs. Recently rav1e has eliminated an implicit copy of a large type and gained speed thanks to it.
PR link
pnkfelix commentedon Oct 26, 2021
Just wanted to note that we did add a lint that highlights all moves larger than a configured limit #83519