C has bitfields. Bitfields are great, because writing shifts and other bit operations is a pain and error-prone. There's plenty of fancy macros for emulating bitfields, sure, but it's limited one specific kind of packing within a struct.
I'm interested in how far we can take cramming down fields in a struct on the assumption that you can't take references to them. For example, consider the classic, extremely wasteful struct:
struct Foo {
a: Option<u32>,
b: Option<u32>,
c: Option<u32>,
}
A whole 93 bits of this struct are wasted, because every one of those options expects to be able to be the referent of a reference, and thus need to have identical layouts, so the presence bit gets rounded up to 1. However, if we aggregated all the presence bits into a byte, we save 64 bits! However, this means that you can't get a reference to those Options, because they're discontinuous!
You see this kind of optimization a lot, especially in code that needs to really pack its representations for efficiency, like a regular expressions engine or generated protobuf code. It would certainly be nice to have compiler support, similar to niche-finding, for saying "hey, I'm never going to need a reference to this field, can you please lay it out in some crazy way?
Here's a strawman to illustrate what I want. Suppose we add a #[flat]
annotation that you can put on a field:
struct Foo(u8, u16);
struct Bar(#[flat] Foo, u8);
Under normal circumstances, Bar
is going to take up three u16
s, because Foo
needs to be padded out, even if Rust decided to order the u8
after the u16
. However, because we've requested that Foo
be flattened, the definition is instead treated as if it were
struct ActualBar(u8, u16, u8);
We save two bytes, at the cost that &bar.0
is now a hard error. The field bar.0
instead has Cell
semantics. However, &bar.0.1
is valid, because the u16
will be aligned, as would be the case in ActualBar
.
The local struct version is easy, but it gets a bit messier when you involve pre-existing types. We want
struct Foo {
#[flat]
a: Option<u32>,
#[flat]
b: Option<u32>,
#[flat]
c: Option<u32>,
}
to be laid out as (u32, u32, u32, u8, padding)
or similar. But suddenly, because you can't take references, you can't do simple things like foo.a.is_some()
, because that requires a reference to a
. For now, I think it's best to just assume you can only flatten Copy
types, but it's probably possible to extend to non-Copy
types in some cases; the Copy
case is probably the most interesting one. You should also be able to write
match &foo.a {
Some(a) => a, // types at &u32
None => ...,
x => /* not allowed! */,
}
but I'm not sure how to best specify this in a way that doesn't feel unnecessarily magical...
Open questions from my perspective:
- Should "flatness" be a property of a field, or of a type? For fields, it means that you can make arbitrary existing types flat without incurring the no-references-allowed penalty. For a type, it means we can make it a trait, which could be useful in generic contexts, but it's not clear to me how.
- How do we deal with matching on a flat enum field to extract references into it?