We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Performance and Data Layout step 4 of 20
Type sizes, enum layout and niche optimisation
Enum size is a silent cost multiplier. Every Result<T, E> in a hot loop
carries max(size_of::<T>(), size_of::<E>()) bytes, whether or not the error
case ever happens. Every Vec<Message> pays for the fattest variant on every
element. And because nothing warns you at the call site, the cost is invisible
until you measure it.
size_of is a const fn, so unlike most performance questions, layout is
provable at compile time. That makes it one of the few topics where you
can test your understanding exactly rather than argue about it.
How an enum is laid out
enum Message {
Ping,
Code(u32),
Blob([u8; 512]),
}
A value of this type must be able to hold any variant, so it needs room for
the largest one, plus a discriminant telling you which it currently is,
plus padding to satisfy alignment. That is 520 bytes for a Ping. Move it and
you memcpy 520 bytes. Put a thousand of them in a Vec and you have allocated
half a megabyte to store mostly nothing.
The fix is to Box the offending variant. The rare large case pays a heap
allocation and a pointer chase; every other case gets a type that fits in a
register pair. clippy’s large_enum_variant exists precisely to point this
out, and result_large_err is the same lint aimed at the error half of a
Result.
Niches, which are the elegant part
size_of::<Option<&u8>>() == size_of::<&u8>()
Eight bytes, not sixteen. There is no separate discriminant at all.
A reference is never null, so the bit pattern 0 is an invalid value for
it — a niche. Option notices, and encodes None as that otherwise-illegal
pattern. Some(x) is just x. This is why Option<&T> is a zero-cost
nullable pointer and why FFI can pass one where C expects T*.
The same applies to Box<T>, &mut T, NonZeroU32, char (which has
invalid scalar values above 0x10FFFF), and — importantly here — enums with
spare discriminant values. A three-variant enum whose tag is stored in a
byte has 253 unused patterns, so Option<Message> can hide None in one of
them and stay the same size.
Option<u64> gets no such luck: every one of the 2^64 patterns is a valid
u64, so there is nowhere to hide and the type grows to 16 bytes (8 for the
value, 1 for the tag, 7 for padding).
What to write
Restructure Message so that both of these hold, without changing
describe at all:
const _: () = assert!(size_of::<Message>() <= 16);
const _: () = assert!(size_of::<Option<Message>>() == size_of::<Message>());
Those are const assertions. They run during compilation, and when one fails
you get E0080, evaluation of constant value failed — a unit test that
executes before your program exists. Get used to them; they are the cheapest
regression test in the language.
Then fill in the report:
pub fn layout_report() -> Vec<(String, usize, usize)>
returning (name, size_of, align_of) for these eight types, in this order:
Message, Option<Message>, Box<[u8; 512]>, Option<Box<[u8; 512]>>,
&u8, Option<&u8>, u64, Option<u64>.
Why the tests assert relationships, not numbers
The grader checks size_of::<Option<&u8>>() == size_of::<&u8>(), not
size_of::<Foo>() == 24. That is deliberate and it is the habit to copy.
repr(Rust) layout carries no guarantees. The compiler may reorder
fields, choose a different discriminant width, or change its mind in the next
release. Every relationship asserted here is guaranteed — the niche
optimisations for references and Box are documented — while the absolute
numbers are not. Write the assertion that stays true.
(-Zprint-type-sizes prints the compiler’s actual layout decisions, and it is
nightly-only. On stable, size_of probes like these are the tool.)
An honest caveat about the payoff
It is tempting to promise that shrinking an enum makes your program faster.
Measured, on this toolchain: Result<u64, [u8; 512]> against
Result<u64, u8> in a tight loop came out 0.4825 ms versus 0.4795 —
indistinguishable, because LLVM hoisted the error construction out of the loop
entirely.
Large variants are a real problem for stack pressure, memory footprint and
API ergonomics. They are not reliably a microbenchmark problem. Do not
promise a speedup you cannot show; the honest claim is the one about memory
and about Vec density, and it is strong enough on its own.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.