We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Smart Pointers and Interior Mutability step 4 of 26
`large_enum_variant`: boxing the fat variant
Route a stream of protocol messages.
pub fn route(msgs: Vec<(String, String)>) -> Vec<String>
Each input pair is (kind, payload) and produces one output line:
| kind | becomes | output |
|---|---|---|
"ping" |
Msg::Ping |
"ping" |
"text" |
Msg::Text(payload) |
"text:<payload>" |
"blob" |
Msg::Blob(512 bytes) |
"blob:<sum of the 512 bytes>" |
| anything else | nothing |
"drop:<kind>" |
The blob’s 512 bytes are the payload’s bytes repeated cyclically, or all zeroes when the payload is empty.
This one starts already working
Read the starter. It compiles. It produces the right answer for every test case. It is also rejected, and the rejection is the entire lesson:
error: large size difference between variants
--> src/lib.rs:3:1
|
3 | / pub enum Msg {
4 | | Ping,
5 | | Text(String),
| | ------------ the second-largest variant contains at least 24 bytes
6 | | Blob([u8; 512]),
| | --------------- the largest variant contains at least 512 bytes
7 | | }
| |_^ the entire enum is at least 520 bytes
Why that is a real cost and not a style opinion
An enum is laid out as a tag plus enough room for its largest variant, and
that size applies to every value of the type. A Msg::Ping carries no data
at all, yet in the starter’s layout a Msg::Ping occupies 520 bytes. So does
every Msg::Text. A Vec<Msg> of a thousand pings allocates half a megabyte
to store a thousand tags.
It gets worse than the memory. Every time a Msg is moved — returned from a
function, pushed into a vector, sent down a channel, matched by value — the
compiler emits a 520-byte copy, because moves in Rust are memcpy of the
whole value. A modern cache line is 64 bytes; a Msg spans nine of them. The
small, common messages pay the price of the big, rare one.
The fix the lint suggests is Box<[u8; 512]>. The variant shrinks from 512
bytes to one pointer, the enum shrinks from 520 bytes to 32, and every Ping
and Text gets cheap again. You will need Box::new(buf) at the construction
site; the match arm needs no change at all, because b.iter() autoderefs
straight through the box.
The honest caveat: boxing is not free
Do not walk away thinking “big variant, add Box“. You have traded:
- Gained a smaller type for every value, which is a saving on every move, every element of every collection, and every cache line.
- Paid one heap allocation and one pointer indirection every time you construct or read the big variant.
That trade is good when the big variant is rare — an error payload, an
occasional bulk frame, a Result‘s Err arm — and it is a pessimisation
when the big variant is the common one. If ninety per cent of your messages
are blobs, boxing means ninety per cent of your messages now cost a malloc
and a pointer chase to save bytes on the ten per cent that do not.
Clippy cannot know your traffic mix. The lint’s threshold is a heuristic, and
it is configurable through clippy.toml — which a single-file submission
cannot supply, so the built-in default is what grades you here. That is also
the honest answer to “is clippy always right”: no, and this is a lint where
a comment justifying #[allow(clippy::large_enum_variant)] is a legitimate
outcome in a real codebase. It is not the outcome here, because the point of
the exercise is to make the layout change.
The same lint, wearing a different hat
result_large_err is large_enum_variant for the specific case that bites
hardest: a Result<T, E> whose E is enormous. Since Result is an enum,
a 200-byte error type makes every successful return 200 bytes too — on the
happy path, which is the path you took ninety-nine per cent of the time. The
idiomatic answer is Box<MyBigError> or Box<dyn Error>, which is why so
much real Rust returns Result<T, Box<dyn Error>>.
And note which lints do not fire here. box_collection and vec_box
exist to stop you boxing things that are already heap-allocated —
Box<Vec<u8>>, Vec<Box<i64>>. A [u8; 512] is a genuine inline array, so
boxing it is the right call rather than a double indirection. Knowing which
side of that line you are on is the skill.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.