- Boyutları farklı variant'ları olan enum/tagged union yapıları büyük miktarda depolandığında, en büyük variant'a göre alan ayrılması nedeniyle
Vec·HashMapiçinde padding ve parçalanma maliyeti artar - Zig,
comptimeve tip yansıması sayesinde alan boyutu·hizalama·discriminant bilgilerini inceleyip, enum kapsayıcılarını bellek yerleşimine göre jenerik biçimde dönüştürebilir - Basit bir
Vec<Enum>yaklaşımında her öğe en büyük variant kadar yer kaplar; SoA ise tag padding'ini azaltır ama değer alanındaki variant fragmentation sorununu ortadan kaldırmaz - Aynı boyuttaki variant'ları gruplayan dense AoVA, örnek enum'da 15 vektörü 2·4·8 baytlık 3 kümeye indirir; ancak aynı allocation içinde birden fazla variant karıştığında tür güvenli iterasyon zorlaşır
- Rust proc macro'ları tip boyutu·hizalama bilgilerine erişmekte zorlanır ve generic dizi uzunluğu hesaplamasında da kısıtlarla karşılaşır; bu yüzden Zig'in tipe duyarlı staging yaklaşımı, sistem kodunda bellek verimliliğinin bileşenler arasında daha iyi birleştirilebildiğini gösterir
Rust enum dizileri neden alan israf eder
- Boyutları farklı variant'lara sahip enum/tagged union türleri, en büyük variant'ı barındırabilecek kadar bellek ayırmak zorundadır
- Örnek
Fooenum'uu8,u16,u32,u64variant'larına sahiptir ve tag ile hizalama nedeniyle tür boyutu 16 bayt olur - Bu tür enum'lar
Vecya daHashMapiçinde çok sayıda tutulduğunda, her öğe en büyük variant'a göre yer kaplar; bu da padding ve parçalanmayı artırır - Tag'i ayrı bir allocation'da tutan struct of arrays(SoA) dönüşümü bazı padding maliyetlerini azaltabilir, ancak variant boyutu farklarından doğan değer alanı parçalanmasını tamamen gidermez
- Rust'ta da belirli bir enum için özel veri yapısı yazılabilir; ancak rastgele bir enum için mümkün olan en bellek verimli jenerik veri yapısını kurmak zordur, hatta pratikte neredeyse imkânsızdır
- proc macro'lar üçüncü taraf tipler veya type alias'lara
#[derive]eklemekte zorlanır ve bileşenlenebilirlikleri düşüktür - Tip farkındalığı yoktur;
generic_const_exprtabanlı dolaylı çözümler ise ayrıntılıwherekoşullarını çağrı grafiğine yayar ve generic type parameter'larla iyi uyuşmaz
- proc macro'lar üçüncü taraf tipler veya type alias'lara
Derleyici AST'lerinde sorunun daha belirgin olmasının nedeni
- Verimli enum dizilerine yönelik en büyük motivasyonlardan biri, derleyici AST'lerinin bellek kullanımıdır
- Büyük AST'ler derleme sırasında bellek gecikmesine ve cache eviction'a yol açarak frontend performansına ciddi maliyet getirir
- Chandler Carruth'un Carbon compiler videosunda, parse edilmiş clang AST'sinin kaynak koddan sık sık 50 kat fazla bellek tüketebildiği belirtilir
- Rust'ta ifade düğümlerini temsil eden örnek yapı bir
Exprenum'udurUnitNumberBinary(Operation, ExprId, ExprId)Ident(Symbol)Eval(ExprId, ExprSlice)BlockExpression(ExprId, StatementSlice)
- OCaml'da çalışma zamanı sistemi ve GC bellek yönetimini üstlendiği için, açık indirection olmadan özyinelemeli veri tipleri ifade edilebilir
- Rust'taki
Vec<Expr>içinde tüm öğelersizeof(Enum)kadar yer kaplar; buna en büyük variant boyutu, tag ve padding dahildir
SoA ve AoVA ile parçalanmayı azaltmak
- Basit bir 3-variant enum'da üyeler 8·16·32 bit boyutlarında olduğunda, sıradan
Vec32 bitlik variant ve hizalama koşullarına uyum sağlamak için tüm öğelere büyük alan ayırır - Yaygın bir iyileştirme, tagged index gibi yöntemlerle enum variant'ının kendisini küçük tutmaktır
- Rust compiler'ın
tagged_indexcrate'i - small-string optimization örnekleri
- Dil çalışma zamanları, GC, derleyiciler, oyun motorları ve OS kernel gibi yüksek performanslı kodlarda sık görülen bir optimizasyondur
- Rust compiler'ın
- Kapsayıcıyı değiştirip discriminant ile değeri ayrı allocation'larda tutan SoA yaklaşımı da mümkündür
- self-hosted Zig compiler bu yöntemi kullanır
- Tag'den kaynaklanan padding azalır, ancak union değer koleksiyonunda hâlâ variant fragmentation kalır
- Zig'in staged compilation modeli, rastgele tipler için SoA dönüşümü yapan kapsayıcıların jenerik olarak kurulmasına imkân verir
- Rust ise
soa_derivegibi proc macro'lara bağımlıdır; ayrıca üçüncü taraf tipin kaynağını değiştirmeden#[derive]ekleyememe kısıtı vardır
Variant başına dizi ve boyuta göre kümelendirme
- Değer alanındaki parçalanmayı daha da azaltmak için her variant için bir vektör tutulabilir
- Ekleme sırasında, enum tag'i ile ilgili variant dizisindeki index'i birlikte taşıyan bir tagged index döndürülür
- Bu desen array of variant arrays(AoVA) olarak adlandırılır
- AoVA, Rust'ta proc macro ile, Zig'de ise
comptimeile uygulanabilir - Variant sayısı fazlaysa ve aynı boyuta sahip birden çok variant varsa, variant başına vektör yaklaşımı aşırı sayıda vektör üretir
- Örnek
Fooenum'unda 15 variant vardır - Variant başına vektör yaklaşımı 15 ek vektör gerektirir
- Yeniden ayırma ve sistem çağrısı sayısı artabilir; amortization açısından da naive
Vec'e göre daha fazla bellek gerekebilir - Vektörler bellekte rastgele dağılabildiğinden cache conflict olasılığı artabilir
- AoVA kapsayıcısının kendisi de çok bellek kullanarak onu içeren struct'ı şişirebilir
- Örnek
- Boyuta göre gruplandığında, örnek enum 2 bayt, 4 bayt, 8 bayt olmak üzere üç kümeye ayrılır
c_2: Vec<[u8; 2]>,AileDarasını depolarc_4: Vec<[u8; 4]>,EileIarasını depolarc_8: Vec<[u8; 8]>,JileOarasını depolar
- Dense AoVA yaklaşımı toplam vektör sayısını %80 azaltabilir
- Farklı variant'lar aynı allocation içinde tutulduğunda, vektör üzerinde tür güvenli biçimde iterasyon yapmak zorlaşır
- Erişim yalnızca ekleme sırasında üretilen tagged pointer üzerinden mümkündür
- Kör iterasyon gerektirmeyen, flattened index tabanlı ağaç yapılarında bu kabul edilebilir bir trade-off olabilir
- Tür güvenli iterasyon gerekiyorsa, padding maliyetini üstlenip tag yeniden eklenebilir
- Padding çok büyükse her variant dizisine SoA dönüşümü uygulanabilir; ancak bu kez vektör sayısı iki katına çıkar
Zig comptime ile gelen bellek yerleşimi bileşenlenebilirliği
- Zig prototipi osmium içinde uygulanmıştır
- Temel mekanizma, alan tipi, bayt boyutu, bit boyutu ve discriminant'ı inceleyen derleyici built-in'leriyle yapılan derleme zamanı yansımasıdır
- Örnek kod
@typeInfo(inner)ile tip türünü denetler ve yalnızca union ise işleme devam eder- union alanları üzerinde dolaşır
@max(field.alignment, @sizeOf(field.type))ile gerekli alanı hesaplar- Boyut bilgilerini stack-allocated bir vektörde saklar
- union alanından cluster index'e giden eşlemeyi kurar
- union değilse compile error üretir
- Tam kod parçası ilgili kaynakta bulunabilir
- Aynı örneği Rust proc macro ile kurmak temelde mümkün değildir
- proc macro'lar tipin size veya alignment bilgisine erişemez
- Belirli bir enum için cluster hesaplayan
const fnüretilebilir, ancak bu generic bir tipin dizi uzunluğunu belirlemek için kullanılamaz
- Rust'ta jenerik bir kapsayıcı uygulaması, verilen tipin enum mu struct mı olduğuna göre koşullu biçimde değiştirilemez
- Zig'de kavramsal olarak
T.isEnum()sonucuna göreEfficientEnumArray<T>ileEfficientStructArray<T>arasında seçim yapılabilir - AoVA uygulaması da enum özelliklerine göre seçilebilir
- Örneğin, farklı variant'ları birlikte yerleştirmenin ancak vektör sayısını %90'dan fazla azalttığında anlamlı olduğuna karar veren bir özelleştirme yapılabilir
- Maksimum capacity derleme zamanında biliniyorsa, tip üretim fonksiyonu tagged index için gereken bitwidth değerini belirleyebilir
- Bu tagged index başka bir veri yapısının, örneğin başka bir enum'un içine girerse, boşta kalan bitler discriminant için kullanılabilir
- Zig, gereken bit sayısını somut olarak ifade etmeye izin vererek kodun diğer bölümlerinin de bu bilgiyi doğal biçimde kullanmasını sağlayan bileşenlenebilir bellek verimliliği sunar
- implicit widening integer coercion sayesinde, farklı bitwidth'e sahip API'lerle çalışırken de kullanım kolaylığı korunur
- Verimlilik ve zero-cost abstraction'ı önemseyen sistem programlama dilleri için, staged programming'e, özellikle de Zig'in
comptimeyaklaşımına yeniden bakmak gerekir
1 yorum
Hacker News yorumları
Depolama verimliliği iyi olan ve öğe dolaşımını da koruyan başka bir strateji var. İlk vektör etiket listesi, ikinci vektör her öğenin bayt ofseti, üçüncüsü ise vektörden çok ikinci vektörün işaret ettiği sıkıştırılmış variant verisi olacak şekilde tutuluyor.
Böylece yazarın nihai çözümüne göre vektör sayısı yarıya iniyor (6'ya karşı 3), sıralama nedeniyle gerekli olan durumlar dışında padding baytları boşa harcanmıyor ve türden bağımsız olarak veri bellekte sıralı yerleştiği için cache dostu biçimde dolaşılabiliyor. İndeksle öğelere O(1) erişim de mümkün. Genel olarak heterojen veri için
Vecbenzeri performans özellikleri taşıyor.T'lere kıyasla oldukça fazla yer kaplayabilir. Örneğin 64 bitsize_tileuint8_t Tbirleşimi böyle; yalnızca ofset boyutuna dikkat edilirse makul bir yaklaşım gibi görünüyor.Bu AoVA veri yapısının gerçekte nasıl çalıştığını merak ediyorum. Dizi açısından indeks aritmetiğinin artık anlamlı olmayabileceği düşünüldüğünde indeks tabanlı erişimi kaybetmiyor muyuz? Dolaşım da ekleme sırasını korumayacak gibi.
Bu bağlamda cache özellikleri daha iyi olan TLV(tag-length-value)'nin daha yaygın kullanıldığını düşünüyorum. Uzunluk etiketten çıkarılabilir de; en azından anlamlı bir ileri yönlü dolaşım sağlar.
getdents,inotify, Netlink mesajlaşmasına bakın.Önceki SoA yerleşimiyle karşılaştırıldığında genel sıra değil, kısmi sıra oluşuyor. Ekleme sırasında enum etiketiyle ilgili variant dizisi içindeki indeksi birlikte içeren etiketli bir indeks döndürülen bir yapı. Bu yüzden sıralı erişim burada kapsam dışı görülüyor gibi. Her öğede global bir indeks saklanırsa sıralı dolaşım geri getirilebilir, ama sıralı rastgele erişime yine de yardımcı olmaz ve kodun epey dallanmalı olması muhtemel.
Nesneleri boyutlarına göre saklama yöntemi garbage collector'larda ve genel amaçlı ayırıcılarda da kullanılır. Olası nesne boyutlarının tümünü bilmekten verim kazanılabilir; arena gibi daha basit serbest bırakma yöntemleriyle de verim elde edilebilir.
Bu durumda diziler, heap benzeri bir yapının bir bileşeni, yani arena gibi görülebilir. Maliyeti, indeksin
(tag_idx, va_for_tag_idx)gibi 2 boyutlu olması gerekmesidir. Ancak etiket sayısı derleme zamanında bilindiğinden,tag_idxüstteki 4~5 bite koyulupva_for_tag_idxkalanını kullanacak şekilde paketlenerek depolama optimize edilebilir. Referans: https://www.cs.cornell.edu/~asampson/blog/flattening.htmlRust'ın pattern matching'inin kendi depolama yapısına sahip açık, birinci sınıf, hardcode edilmiş bir nesne tipi olmak yerine, rastgele yapıların uyabileceği tip sistemindeki trait benzeri bir biçimde daha fazla ifade edilememesi biraz üzücü.
Yakın zamanda bu yazıdaki gibi bir AST de, bir opcode/bytecode yorumlayıcısı da uyguladım; Rust enum'larının ikisi için de tamamen ideal olmadığını hissettim. AST'de tüm statement düğümlerine satır numarası/sütun özellikleri eklemek istiyordum;
Stmtenum'unun tüm durumlarına satır/sütun koyunca boilerplate kirli hale geliyor, enum'u özgün enum ile satır/sütun özelliklerini birlikte taşıyan yeni birStmtstruct'ıyla sarmalayınca da çok refactoring gerekiyor ve zarif olmuyordu. Opcode tarafında da pattern matching yapılan bir Rust enum'unun VM opcode yorumlayıcısı performansı açısından ideal encoding olduğu söylenemez, ama dil bu yöne teşvik ediyor ve destructuring pattern özelliği çok çekici. İstenen düşük seviye implementasyonu kullanırken yine de pattern matching özelliğini elde etmeyi sağlayacak tip sistemi iyileştirmelerine yer var gibi görünüyor.https://en.wikipedia.org/wiki/Structural_type_system
computed gotoeklentisi vardı; Rust'ta ise muhtemelen fonksiyon pointer'ları ve tail call optimizasyonunu zorlayacak bir şeye ihtiyaç olurdu.Böyle bir dolaylı atlama her opcode implementasyonunun başına koyulursa, CPU'nun OOP nedeniyle sahip olduğu dolaylı atlama tahmincisi farklı opcode sonları için ayrı modeller tutabilir ve tahmin başarı oranı artabilir. Bir sonraki komutun kendisini tahmin etmek zor olsa da, örneğin test'ten sonra branch gelme olasılığı çok daha yüksek olabilir. Ancak stack makinesinde stack'in tepesini register'da saklamak gibi başka tekniklerin daha önemli olacağını düşünüyorum; yukarıdaki tekniğin bugün hâlâ anlamlı olup olmadığından da pek emin değilim.
“Ayrıştırılmış clang AST’si düzenli olarak özgün kaynak koddan 50 kat daha fazla bellek yiyor” ifadesi oldukça büyük görünüyor, ama eksik bağlam ne kadar iyileştirilebileceği. Her token’ın kaynak konumunu korumak ve AST’den düzgünce geri kazanılabilecek kadar bilgi kodlamak gerekiyorsa, özgüne göre ideal artış oranının 1,5 kat mı yoksa 15 kat mı olduğunu merak ediyorum
Kullanıcı dostu ve aynı zamanda derleyici geliştiricileri için de dostu bir dilde ideal kaynak→AST şişme oranının ne olduğunu söylemek zor, ama 50 kat da çalışıyor. Özgün metinde 50 katlık şişme oranı, belirli bir optimizasyonu otomatikleştirmek için motivasyon olarak kullanılıyor. Rust’ın enum vektörleri enum değerlerini otomatik olarak tag ve opak değerlere ayırıp, Zig’de özgün metnin yaptığı gibi dizi-struct biçiminde saklayabilse ilginç olurdu.
unsafekullanımını gizleyecek pek fazla yer de yok gibiÇoğunluğu
[]karakterlerinden ya da0,karakterlerinden oluşan belgelerde en yüksek ek yük yaklaşık 8 kat gibi görünüyorKaynak baytları: 139 KiB, token: 24646 adet (120 KiB), AST düğümü: 10998 adet (140 KiB). Her token 5 baytla oldukça minimize edilmiş durumda (1 bayt tag + 4 bayt dosya ofseti) ve AST düğümleri de yoğun ve düzensiz biçimde kodlanmış; bu durumda düğüm başına yaklaşık 13 bayt. Bu minimal kodlamaya rağmen parse tree, kaynak dosya boyutunun neredeyse 2 katı oluyor. Yine de 2 kat, 50 kattan çok daha iyi. Kaynak:
zig ast-check -t lib/std/zig/Parse.zig | head -n7Bu problem alanı paketleme probleminin bir varyantı gibi geliyor
Nihai sonuç olan, insanların işlemesi kolay yapıdan başlayıp bellek israfını azaltan, hizalama kurallarına uyan ve uzamsal yerelliği artıran veri yapısı önerileri üretebilsek güzel olurdu. https://en.wikipedia.org/wiki/Packing_problems
proc macro’ların derleyiciye bilgi sorabilecek şekilde gelişmesi iyi olurdu. Bunun için ek derleme aşamaları konusunda dikkatli bir tasarım gerekir, ama “bu struct şu trait’i uyguluyor mu”, “somut olarak uygulanmış tüm trait’lerin listesini ver” gibi şeyler proc macro içinde çoğu zaman çok kullanışlı olur
Yazının yalnızca bir kısmını anladım, ama Rust ile spreadsheet motoru yazmak isteyen biri olarak çok ilgili görünüyor. Hücre değerleri için şöyle bir biçim gerekiyor
pub enum Expr { Number(i32), String(String), Reference(Position), Binary(Box, char, Box), Function(String, Vec), }Okumaya ve çalışmaya devam edeceğim; başvurulacak kaynak varsa memnun olurum
Oyun tarafında sık kullanılan bir teknik, struct dizisini (AoS) dizilerin struct’ına (SoA) bölmektir. Örneğin
struct Humans { healths: Vec, ammo: Vec, … }şeklinde tutarsanız, her vektörün i’inci indeksi AoS yerleşimindeki i’inciHumanolur. Bu paralel vektörler yalnızca bir örnek; en iyi verimlilik bu değil, çünkü her alan için uzunluk ve kapasite kayıtları yinelenerek israf yaratır. Bu yazı temelde benzer fikri enum’lara otomatik olarak uygulamaya çalışıyor ve Rust’ta bunu aynen yapmak zor. Bu problemin pratikte ne kadar büyük olduğu biraz abartılmış da olabilir. Spreadsheet tarafında bunu şimdilik yalnızca olası bir optimizasyon olarak akılda tutmalı, önce hız için mi yoksa sadelik ve anlaşılabilirlik için mi geliştirdiğine karar vermelisin1 milyon × 1 milyon hücreye izin verip doldurulmamış tüm hücrelerde
nullsaklarsan bellek tükenir. Bu yüzden hücre içeriklerini seyrek saklama yöntemini düşünebilirsin. Bir yöntem,hashbrowngibi bir hash map implementasyonu kullanmak. Bu yazının noktası düşük seviye ayrıntılar; en baştan hash map ile başlayıp başlangıçtaki bellek kısıtlarını aşarsan şimdilik bunu derinlemesine düşünmene gerek yokEn zor tekil problem değerlendirme stratejisi
https://doc.rust-lang.org/reference/type-layout.html#the-alignment-modifiers nasıl olur
Örnek kodda bir bug var gibi
field_map[idx] = svec.len - 1;sveczaten son entry olmayan bir yerdesizeiçeriyorsa yanlış olur