Rust enums can surprisingly be smaller than expected. While naively, one might assume an enum's size is determined by the largest variant plus a discriminant to track which variant is active, the compiler optimizes this. If an enum's largest variant contains data with internal padding, the discriminant can sometimes be stored within that padding, avoiding an increase in the overall size. This optimization applies even when using #[repr(C)]
or #[repr(u8)]
, so long as the layout allows it. Essentially, the compiler cleverly utilizes existing unused space within variants to store the variant tag, minimizing the enum's memory footprint.
This blog post by James Fennell explores a fascinating optimization performed by the Rust compiler regarding the size of enums, specifically how it leverages the niche-filling technique to reduce memory footprint. The author begins by establishing the fundamental concept of enum representation in memory. Enums, by their nature, can hold values of different types, meaning the compiler needs to allocate enough space to accommodate the largest possible variant. This often results in padding if the variants have significantly different sizes.
The post then dives into the concept of "niche filling." A niche, in this context, refers to a bit pattern or value that a specific data type cannot represent. For instance, references in Rust are guaranteed to be non-null. This means the all-zeros bit pattern (representing a null pointer) becomes a niche that can be exploited. The compiler cleverly uses these niches to store smaller enum variants, thus avoiding the need for additional padding and reducing the overall size of the enum.
Fennell illustrates this optimization with a concrete example involving an enum containing a reference and a boolean. Naively, one might expect this enum to require the size of a reference plus a boolean (e.g., 8 bytes for a 64-bit pointer and 1 byte for a boolean, potentially padded to 16 due to alignment). However, the Rust compiler recognizes that the null pointer value is a niche for references. It then assigns this niche bit pattern to represent the boolean variant, allowing the entire enum to fit within the size of a single reference (e.g., 8 bytes). This effectively eliminates the need for extra space to store the boolean value, leveraging the unused bit pattern of the null pointer.
The post further explains that this optimization doesn't only apply to references. It extends to other types with niches, such as NonZeroU8
and NonZeroUsize
, demonstrating a broader applicability of this memory-saving technique. The author provides clear code examples and diagrams to visually illustrate the memory layout before and after the optimization, highlighting the efficiency gains.
Finally, the post acknowledges limitations and complexities. The niche-filling optimization is not always guaranteed. Factors like generic types and platform-specific representations can influence whether the compiler can successfully implement it. Even so, the article clearly demonstrates a powerful optimization employed by the Rust compiler to minimize the memory footprint of enums, showcasing a nuanced understanding of data representation and clever utilization of unused bit patterns.
Summary of Comments ( 1 )
https://news.ycombinator.com/item?id=43616649
Hacker News users discussed the surprising optimization where Rust can reduce the size of an enum if its variants all have the same representation. Some commenters expressed admiration for this detail of the Rust compiler and its potential performance benefits. A few questioned the long-term stability of relying on this optimization, wondering if changes to the enum's variants could inadvertently increase its size in the future. Others delved into the specifics of how this optimization interacts with features like
repr(C)
and niche filling optimizations. One user linked to a relevant section of the Rust Reference, further illuminating the compiler's behavior. The discussion also touched upon the potential downsides, such as making the generated assembly more complex, and how using#[repr(u8)]
might offer a more predictable and explicit way to control enum size.The Hacker News post titled "A surprising enum size optimization in the Rust compiler," linking to an article about enum size optimization in Rust, has generated several comments discussing the nuances of this optimization and its implications.
Several commenters delve into the specifics of the niche-filling optimization discussed in the article. One commenter explains how this optimization interacts with the
repr
attribute in Rust, clarifying that while#[repr(u8)]
forces the enum to be represented as au8
, the niche-filling optimization still applies when possible, even without explicitly setting a representation. They provide an example of how this works in practice, illustrating that even with#[repr(u8)]
, the enum can still be optimized to a smaller size if its variants allow.Another commenter discusses the trade-offs between size optimization and runtime performance, pointing out that while smaller sizes are generally desirable, they can sometimes lead to increased runtime costs due to extra operations needed for encoding and decoding the optimized representation. This commenter also explains how the Rust compiler's zero-cost abstraction principle influences these decisions.
The discussion also touches on the complexity of enum representations and the challenges in predicting the final size. One commenter mentions that the compiler's behavior can sometimes be counterintuitive, leading to unexpected sizes. They provide an example where adding a field to a struct within an enum variant can surprisingly decrease the overall size of the enum due to the way niche-filling interacts with alignment requirements.
Furthermore, a commenter contrasts Rust's approach with that of C/C++, highlighting the differences in enum representation and the potential for optimization in each language. They note that while C/C++ enums typically default to the size of an integer, Rust's approach allows for more compact representations, especially when niche-filling is possible.
Finally, the topic of
Option<NonZeroU8>
is brought up, with commenters explaining how the compiler can optimize its size down to a single byte because theNone
variant can occupy the niche value of zero, while theSome
variant stores the non-zero value directly. This example illustrates a common and practical use case of niche-filling optimization in Rust.Overall, the comments section provides valuable insights into the intricacies of Rust's enum size optimization and its practical implications. They offer a deeper understanding of the trade-offs involved, the compiler's behavior, and how these optimizations can impact code size and performance.