This is a good point, but the issue is exaggerated, I think.
TL;DR: all the optimizations in the first post of that issue are sound with internal pointers, and they can push down the size of your example Servo enum to an optimal 16-bytes, with values to spare for further nesting in e.g. Option.
Specifically, all of the proposed optimizations in my post are using forbidden values, and not padding. Nothing can be broken by internal writes to x in Foo(x) if the repr of Foo(x) is exactly that of x (plus maybe some values that are outside of sizeof(x).
Here’s my analysis of the proposals:
Use Undefined Values in Other Primitives
Some(Some(false) = 0
Some(Some(true)) = 1
Some(None) = 2
None = 3
As you can see, writes by inner pointers can never corrupt the outer variant (unless the outer variant was already overwritten, in which case the write is violating type-safety anyway).
Use Multiple Invalid-Value Fields
Some(Some(ref, ref)) = (ref, ref)
Some(None) = (0, 1)
None = (1, 0)
Still no way to corrupt externally.
Support More than a Single Bit
(I’ve already shown this in the first example)
Use all other fields as free bits when there exists another forbidden field
Some(Some((ref, x)) = (ref, x)
Some(None) = (0, 0)
None = (0, 1)
Still good.
Use the Fact that Void is Statically Unreachable
You can’t “write” Void, so there’s no concern for overwriting outer things
Support Enums that have More than One Non-C-Like Variant
enum Foo {
A(&u8, &u8),
B(&u8),
}
A(ref, ref) = (ref, ref)
B(ref) = (ref, 0)
Still good.
Treat any True Discriminant Tags as a Type with Undefined Values
Some(Some(x)) = (0, x)
Some(None) = (1, 0)
None = (2, 0)
Still good.
Let’s apply these optimizations to your enum:
pub enum LengthOrKeyword {
Length(pub enum Length {
NoCalc(pub enum NoCalcLength {
Absolute(pub enum AbsoluteLength {
Px(f32),
In(f32),
Cm(f32),
Mm(f32),
Q(f32),
Pt(f32),
Pc(f32),
}),
FontRelative(pub enum FontRelativeLength {
Em(f32),
Ex(f32),
Ch(f32),
Rem(f32),
}),
ViewportPercentage(pub enum ViewportPercentageLength {
Vw(f32),
Vh(f32),
Vmin(f32),
Vmax(f32),
}),
ServoCharacterWidth(i32),
}),
Calc(
Box<CalcLengthOrPercentage>,
pub enum AllowedLengthType {
All,
NonNegative,
}
),
}),
Keyword(pub enum SizeKeyword {
ClosestSide,
FarthestSide,
ClosestCorner,
FarthestCorner,
}),
}
f32, i32: no optimization opportunity (let’s ignore NaN boxing for now)
AbsoluteLength, FontRelativeLength, and ViewPortPercentageLength, all get the same layout: (tag, u32). Naively they will all get the exact same tag values (0, 1, 2, …), which makes it harder to weave them together.
However a crate-local optimizer might find all the types they’re stored in, and give them disjoint sequences. If this sounds far-fetched, a similar solution can be achieved by simply picking a “random” (deterministic hash) initial value, so that most enums incidentally don’t overlap.
I’ll also assume tags are made to fill all padding, because padding is scary and out of our control, while tags are fully controlled. So in this case we get 32-bit tags, which are very sparse and almost certainly won’t collide. So all of these are 8-bytes in total.
Vw(x) = (0, x)
Vh(x) = (1, x)
Vmin(x) = (2, x)
Vmax(x) = (3, x)
Em(x) = (10, x)
Ex(x) = (11, x)
Ch(x) = (12, x),
Rem(x) = (13, x)
Px(x) = (20, x)
In(x) = (21, x)
Cm(x) = (22, x)
Mm(x) = (23, x)
Q(x) = (24, x)
Pt(x) = (25, x)
Pc(x) = (26, x)
NoCalcLength is the union of all of these, but also adds a ServoCharacterWidth(i32) variant. Trivial local evaluation tells us we can add:
ServoCharacterWidth(30, x)
Length adds a Calc(Box, AllowedLengthType) variant, which will be the primary optimization bottleneck. It must be 16-bytes. AllowedLengthType is C-like and can fit in a byte, so it will probably just be a byte.
(random guessed values)
All = 40
NonNegative = 41
Calc(box, type) = (box, type, 7-bytes of 0)
NoCalc(x) = (0, x)
Finally LengthOrKeyword adds a C-like SizeKeyword. We can just stuff it in some forbidden values of the x in NoCalc(x)
(random guessed values)
ClosestSide = 50
FarthestSide = 51
ClosestCorner = 52
FarthestCorner = 53
Keyword(x) = (0, x, 7-bytes of 0)
So our final result is LengthOrKeyword at 16-bytes, which is just your 11 bytes rounded up to the next biggest power of two; so basically optimal.
Note that the (0, x) case still has plenty of forbidden values, so we can further nest this in other enums without growth.
Also note that matching on this enum is still fairly cheap; e.g. checking for Calc is just box != 0, and ever other variant is just box == 0 && min <= x && x <= max. However in a full match statement these can be coalesced into a decision tree:
match no_calc {
Absolute(x) => ...
FontRelative(x) => ...
ViewportPercentage(x) => ...
ServoCharacterWidth(x) => ...
}
let tag = no_calc.tag
if tag > FontRelative.max_tag {
// Can't be FontRelative or Absolute here
} else {
// Can't be ViewPortPercentage or ServoCharacterWidth here
}
We could also further optimize by making Calc and Keyword use different patterns for the “padding” so we can branch of that instead of some range of values (we can specify it to not be padding, but instead another tag with a definite value).