Rust, bir varyantın geçersiz bit desenleri içeren bir tür içermesi durumunda, enum'lar için ayırt edici depolamayı ortadan kaldırmak üzere niche değer optimizasyonu (aynı zamanda niche doldurma olarak da adlandırılır) uygular. Option<&T> için, None varyantı null işaretçi değeri ile temsil edilir—referansların her zaman null olmaması gerektiği için &T için geçersiz bir bit deseni. Bu, derleyicinin ayırt ediciyi kendiliğinden işaretçi içinde depolamasına olanak tanır ve böylece Option<&T>, tam olarak bir makine kelimesi işgal eder. Bu optimizasyon, geçersiz bit desenlerine sahip herhangi bir tür için uygulanır—NonZeroU32 için 0 gibi, bool için 0 veya 1 dışındaki değerler veya #[repr(C)] yapılarına özgü belirli sentinel değerleri gibi.
Milyonlarca düğüm işleyen bir derleyici için yüksek performanslı soyut sözdizim ağacı (AST) geliştirirken, ebeveyn ve çocuk işaretçilerinden kaynaklanan ciddi bellek baskısı ile karşılaştık. Her düğüm, ebeveynine, sol çocuğuna ve sağ çocuğuna opsiyonel referanslar gerektiriyordu ve başlangıçta Option<Box<Node>> olarak uygulandı.
Option<Box<Node>> kullanmak, 64 bit sistemlerde her işaretçi için 16 bayt maliyet getirdi—Box işaretçisi için 8 bayt ve ayırt edici ve dolgu için 8 bayt. 10 milyon düğümlü bir ağaç için bu, yalnızca bağlantı işaretçileri için 480 megabayt elde etti ve bu, bellek bütçemizi aştı.
Üç yaklaşımı değerlendirdik. İlk olarak, None için null kullanarak Option<Box<Node>>’yi ham işaretçilerle (*mut Node) değiştirmek. Bu, fazlalığı ortadan kaldırdı ama unsafe blokları boyunca riskli olan sarkık işaretçilerle Rust'un güvenlik garantilerini ihlal riski yarattı. İkinci olarak, işaretçiler yerine indekslerle (usize) bir arena ayırıcı kullanmak. Bellek dostuydu, ancak Option<usize> hala usize içerisinde niche eksikliği nedeniyle 16 bayt gerektiriyordu ve indeks aritmetiği API'yi karmaşıklaştırıyordu.
Üçüncü yaklaşımı seçtik: Güvenli ParentPtr soyutlaması içinde sarılı Option<NonNull<Node>>. NonNull<T> adres 0'da bir niche taşır ve bu, Option<NonNull<Node>>'nin 8 bayt kalmasını sağlar. Bellek güvenliğini koruyarak unsafe çözümlemesini sarıcı yöntemler içinde kapsülledik ve sıfır maliyetli soyutlama elde ettik. Bu, AST'nin bellek ayak izini %50 azaltarak, güvenlikten ödün vermeden 256MB kısıtlamamız içinde kalmasını sağladı.
Neden Option<Option<bool>> bir bayt kalırken Option<Option<usize>> 16 bayta genişliyor?
bool, yalnızca geçerli bit desenleri 0 ve 1 olduğu için 254 niche değeri taşır. İlk Option katmanı, None'yı temsil etmek için bir niche (örneğin, 2) tüketir, geriye 253 niche kalır. İkinci Option katmanı, None varyantı için başka bir niche (örneğin, 3) tüketir. Sonuç olarak, Option<Option<bool>> hala bir bayta sığar. Tersine, usize geçersiz bit desenlerine sahip değildir—tüm 2^64 değerleri geçerli bellek adresleri veya verileridir. Niche olmadan, Option<usize> bir ayırt edici bayt eklemelidir, bu da 16 bayta (veri için 8, hizalama için 8) neden olur. İç içe geçmiş Option katmanları, mevcut niche'ler olmadan daha fazla optimize edilemez, bu nedenle Option<Option<usize>> dahili ayırt edici mantıkla 16 bayt kalır.
Neden derleyici, yükleme türünün niche'leri içermesine rağmen, #[repr(C)] işaretleyici ile işaretlenmiş enum'lar için niche optimizasyonunu reddeder?
#[repr(C)] kümesi, C ile uyumlu bellek düzeni ile kararlı alan sıralamasını garanti eder ve belirli bir kayıtta açık ayırt edici depolama ile. C dili standardı, yük verileriyle üst üste gelen ayırt edici değerleri desteklemez—ayırt ediciler belirli bir bellek alanında yer almalıdır, böylece FFI uyumluluğu sağlanır. NonNull<T> gibi bir yapı niche'ler (null işaretçi) içerse de, #[repr(C)] enum'lar bu ayırt edicinin üst üste gelmesini sağlamak için bu imkanlardan yararlanamaz çünkü dış C kodu, sabit bir ofsette açık bir ayırt edici değeri okumayı bekler. Bu kısıtlama, bellek verimliliği pahasına, uyumluluğu korur ve #[repr(C)] altında sizeof(Option<&T>) değerinin sizeof(&T) + sizeof(discriminant) eşit olmasını sağlar; genellikle 16 bayt yerine 8 bayt.
std::mem::discriminant, bellek içinde açık ayırt edici depolama yetersiz olan Option<&T> gibi türler için nasıl çalışır?
std::mem::discriminant, enum varyantını benzersiz bir şekilde tanımlayan opak bir Discriminant<T> değeri döner, bu bellek temsili altında. Option<&T> için, derleyici, ayırt ediciyi işaretçi değerini inceleyerek türeten bir kod oluşturur—işaretçi null değilse Some'ı temsil eden bir sabit döner ve null ise None'ı temsil eden bir sabit döner. Ayrı bir bellek alanı, ayırt edici bir etiketi depolamak için kullanılmasa da, Discriminant türü bu hesaplamayı soyutlayarak varyant karşılaştırmalarını == aracılığıyla gerçekleştirmeye olanak tanır, niche kodlama detaylarını açığa çıkarmadan. Bu, discriminant'ın fiziksel bellek düzeni yerine anlamsal varyant kimliğinde çalıştığını gösterir ve optimize edilmiş ve optimize edilmemiş enum temsilleri arasında tutarlı davranışı sağlar.