Rust maakt gebruik van niche value optimization (ook wel niche filling genoemd) om de opslag van de discriminant voor enums te elimineren wanneer een variant een type bevat met ongeldige bitpatronen. Voor Option<&T> wordt de None variant weergegeven door de nulpointerwaarde - een bitpatroon dat ongeldig is voor &T omdat referenties altijd niet-null moeten zijn. Dit stelt de compiler in staat om de discriminant impliciet binnen de pointer zelf op te slaan, waardoor Option<&T> precies één machinewoord opneemt. Deze optimalisatie is van toepassing op elk type dat niche waarden bezit - ongeldige bitpatronen zoals 0 voor NonZeroU32, waarden buiten 0 of 1 voor bool, of specifieke sentinelwaarden in #[repr(C)] structs.
Tijdens de ontwikkeling van een high-performance abstract syntax tree (AST) voor een compiler die miljoenen knopen verwerkt, hadden we te maken met ernstige geheugendruk van ouder- en kindpointers. Elke knoop vereiste optionele referenties naar zijn ouder, linker kind en rechter kind, aanvankelijk geïmplementeerd als Option<Box<Node>>.
Het gebruik van Option<Box<Node>> kostte 16 bytes per pointer op 64-bit systemen - 8 bytes voor de Box pointer en 8 bytes voor de discriminant en padding. Voor een boom met 10 miljoen knopen kwam dit neer op 480 megabytes alleen voor verbindingspointers, wat ons geheugensbudget overschreed.
We overwogen drie benaderingen. Ten eerste vervingen we Option<Box<Node>> door ruwe pointers (*mut Node) met nul voor None. Dit elimineerde overhead, maar vereiste unsafe blokken door de hele codebase, wat het risico van dangling pointers met zich meebracht en de veiligheidsgaranties van Rust in gevaar bracht. Ten tweede gebruikten we een arena-allocator met indices (usize) in plaats van pointers. Hoewel het cache-vriendelijk was, vereiste Option<usize> nog steeds 16 bytes vanwege het gebrek aan niches in usize, en indexarithmetic compliceerde de API.
We kozen de derde benadering: Option<NonNull<Node>> gewikkeld in een veilige ParentPtr abstractie. NonNull<T> heeft een niche op adres 0, waardoor Option<NonNull<Node>> 8 bytes kan blijven. We encapsuleerden het unsafe dereferenceren binnen de wrapper-methoden, waarbij we de geheugveiligheid behielden terwijl we een zero-cost abstractie bereikten. Dit verlaagde de geheugendruk van de AST met 50%, en voldeed aan onze 256MB-beperking zonder in te boeten op veiligheid.
Waarom blijft Option<Option<bool>> een enkele byte terwijl Option<Option<usize>> uitbreidt tot 16 bytes?
bool heeft 254 niche waarden omdat alleen de bitpatronen 0 en 1 geldig zijn. De eerste Option laag verbruikt één niche (bijv. 2) om None weer te geven, waardoor 253 niches overblijven. De tweede Option laag verbruikt weer een niche (bijv. 3) voor zijn None variant. Bijgevolg past Option<Option<bool>> nog steeds binnen één byte. Aan de andere kant heeft usize geen ongeldige bitpatronen - alle 2^64 waarden zijn geldige geheugenadressen of gegevens. Zonder niches moet Option<usize> een discriminantbyte toevoegen, wat resulteert in 16 bytes (8 voor data, 8 voor uitlijning). Geneste Option lagen kunnen niet verder optimaliseren zonder beschikbare niches, dus blijft Option<Option<usize>> 16 bytes met interne discriminantlogica.
Waarom weigert de compiler niche-optimalisatie voor enums gemarkeerd met #[repr(C)] ook al bevat het payloadtype niches?
De #[repr(C)] attribuut garandeert een C-compatibel geheugenschema met stabiele veldordening en expliciete discriminantopslag op een voorspelbare offset. De C-taalstandaard ondersteunt geen overlappende discriminantwaarden met payloadgegevens - discriminanten moeten op specifieke geheugenslocaties staan om FFI-compatibiliteit te garanderen. Hoewel een struct zoals NonNull<T> niches bevat (nulpointer), kunnen #[repr(C)] enums deze niet benutten om te overlappen met de discriminant omdat externe C-code verwacht een afzonderlijke discriminantwaarde op een vaste offset te lezen. Deze beperking beschermt de interoperabiliteit ten koste van geheugen efficiëntie, waardoor sizeof(Option<&T>) gelijk is aan sizeof(&T) + sizeof(discriminant) onder #[repr(C)], typisch 16 bytes in plaats van 8.
Hoe werkt std::mem::discriminant voor types zoals Option<&T> die geen expliciete discriminantopslag in het geheugen hebben?
std::mem::discriminant geeft een ondoorzichtige Discriminant<T> waarde terug die de enumvariant uniek identificeert, ongeacht de onderliggende geheugensrepresentatie. Voor Option<&T> genereert de compiler code die de discriminant afleidt door de pointerwaarde te inspecteren - een constante teruggeven die Some vertegenwoordigt als de pointer niet-null is, en een constante die None vertegenwoordigt als deze null is. Hoewel er geen afzonderlijke geheugenlocatie een discriminantlabel opslaat, abstraheert het type Discriminant deze berekening, waardoor variantvergelijking via == mogelijk is zonder de niche-encoderingsdetails bloot te leggen. Dit toont aan dat discriminant werkt op de semantische variantidentiteit in plaats van de fysieke geheugensindeling, waardoor consistent gedrag mogelijk is over geoptimaliseerde en niet-geoptimaliseerde enumrepresentaties.