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
tmp
isn't used elsewhere, allowing codegen to treat it like an SSA value, resulting instore(p, load(q))
, which is also what*p = *q
codegens to - for non-primitives, the amount of data being copied doubles, as
tmp
must be in memory
- this is assuming
- correctness: only
*p = *q;
is UB (AFAIK) ifp..p+size
overlapsq..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 Rvalue
s (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]
,N
copies)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 Operand
s 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 Rvalue
s 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