I know that fields in Rust structs are automatically reordered to save memory (unless e.g. #[repr(C)]
is specified).
I was under the assumption that the same is true for enums. Today I was creating a simple helper type (the semantics of the example don't matter, this is about memory layout):
pub enum LazyRwLockGuardVersionA<'a, T> {
Unlocked(&'a RwLock<T>),
Read {
lock: &'a RwLock<T>, // still neccessary for promoting to write lock
guard: RwLockReadGuard<'a, T>,
},
Write(RwLockWriteGuard<'a, T>),
}
While a raw Read/Write Guard uses 16 bytes, this type uses 32. In order to get the compiler to use the niche optimization I tried this version:
pub enum LazyRwLockWriteGuard<'a, T> {
Unlocked(&'a RwLock<T>),
Write(RwLockWriteGuard<'a, T>),
}
pub enum LazyRwLockGuardVersionB<'a, T> {
Read {
guard: RwLockReadGuard<'a, T>,
lock: &'a RwLock<T>,
},
NonRead(LazyRwLockWriteGuard<'a, T>),
}
This successfully brings down the memory usage to 24 bytes.
However, I noticed something weird:
When reversing the order of fields in the Read
variant:
pub enum LazyRwLockGuardVersionC<'a, T> {
Read {
lock: &'a RwLock<T>, // order of fields reversed
guard: RwLockReadGuard<'a, T>,
},
NonRead(LazyRwLockWriteGuard<'a, T>),
}
This type suddenly uses 32 bytes again.
My Rust Version is 1.77, here is a godbolt repro: https://godbolt.org/z/svE5v6Tr8
Are enum fields not allowed to be reordered by the spec, or is the compiler just 'lazy' here?
The default/Rust
type layout is completely unspecified beyond soundness guarantees. The compiler is otherwise free to do whatever it wants, and indeed this does change between compiler versions.
Is there a rough intuition for the algorithm that the compiler currently uses for layouting enums, so I can understand this result, and prevent accidental pessimizations of my code in the future?
If you want to have a guarantee of the layout, you must select a different representation.