-
Notifications
You must be signed in to change notification settings - Fork 13.9k
Description
In today's MIR, an indirect assignment like *p = *q; is similar to, but not exactly the same as:
tmp = *q;
*p = tmp;The differences are:
- performance: they only produce the same codegen for primitives
- this is assuming
tmpisn't used elsewhere, allowing codegen to treat it like an SSA value, resulting instore(p, load(q)), which is also what*p = *qcodegens to - for non-primitives, the amount of data being copied doubles, as
tmpmust be in memory
- this is assuming
- correctness: only
*p = *q;is UB (AFAIK) ifp..p+sizeoverlapsq..q+size- this also likely only affects non-primitives, which have to copy data in memory, but we could decide to ignore the type and always make it UB when they overlap
For the purposes of this discussion, a primitive is:
- scalar (
bool,char, integer, float, or pointer/reference) - vector (SIMD-enabled array of scalars)
Scalar pairs likely also should/need to be included, due to how easy they are to support in any code that already handles scalars, and also due to their use in wide pointers/references.
What's interesting about primitives, though, is that some kinds of Rvalues (the RHS of the assignment) always produce primitive values, because they're primitive operations.
The Rvalue variants which are always primitive, today, are:
Ref(&T/&mut T- may become dependent on custom DSTs in the future)AddressOf(*const T/*mut T- may become dependent on custom DSTs in the future)Len(usize)Cast, other than unsizing (scalar)BinaryOp,UnaryOp(scalar, or maybe also vector)CheckedBinaryOp(pair of integer andbool- only if we consider scalar pairs to be primitive)NullaryOp(SizeOf)(usize)NullaryOp(Box)(Box<T>)Discriminant(integer)
Which leaves these variants as potentially relying on memory operations to write the result:
Use(any type, one copy)Repeat([T; N],Ncopies)Cast, specifically unsizing (any type implementingCoerceUnsized, per-field copies)Aggregate(any ADT, per-field copies)
If we want to remain conservative, we could ignore types for the latter, and just assume that the destination of the assignment cannot overlap any memory read in the Operands of the Rvalue.
We could even cement the distinction by moving the always-primitive operations into a new PrimOp enum, and/or move the other Rvalues to their own statements (e.g. introduce Copy(*p, *q)), but that's more aesthetic than anything for the time being.
At the very least, we should probably document these differences, and make sure that miri only allows overlaps in the cases we don't consider UB (either abstractly, or due to our choice of codegen).
Another interesting aspect of the always-primitive ops is that they're "pure functions" of their operands (other than NullaryOp(Box), I suppose, but that could be replaced with a call to a lang item returning a Box<MaybeUninit<T>>, instead).
This means that if we wanted to, we could replace some of the intermediary locals with an PrimOp DAG, a bit like SSA but without φ (phi) nodes or a strict instruction stream.
All of the necessary ordering would still happen at the statement level (so this is nowhere near as complex as VSDG), but we might see some benefits in scalar-heavy code.
Asides aside, cc @RalfJung @rust-lang/wg-mir-opt