Tarihçe: Go 1.18'den önce, dil parametreli çok biçimliliği desteklemiyordu, bu da geliştiricilerin hem yığın tahsisleri ve kutulama yükü yaratan interface{} veya ikili büyümeye neden olan kod üretimi arasında seçim yapmalarını zorunlu kılıyordu. Generik tasarımında, Go ekibi C++ şablon modelinin tamamen monomorfizasyonunu—her farklı tür örneğinin tekrarlanan makine kodu üretmesi—büyük bulut yerel uygulamalarda binlerce paketi bağlayarak ikili boyut patlaması nedeniyle reddetti.
Problem: Tam monomorfizasyon, Process[int] ve Process[uint] için ayrı montaj blokları oluşturur ve her ikisi de 64-bit tamsayı olmasına rağmen, talimat önbelleğini ve disk alanını israf eder. Öte yandan, kutulama yoluyla generikler uygulamak (Java gibi) değer türlerini yığına zorlayacak, bu da Go'nun sistem programlama alanında kritik olan sıfır tahsis performans özelliklerini yok edecektir. Zorluk, derleme zamanı tür güvenliğini ve sıfır maliyetli değer anlamlarını korumak ve N kat kod çoğaltma sorunundan kaçınmaktaydı.
Çözüm: Go, GC şekil şablonlama ile birlikte çalışma zamanı sözlüklerini kullanır. Derleyici, türleri tam tür kimliği yerine boyut, hizalama ve işaretçi bitmapi tarafından tanımlanan GC şekli ile gruplar. Aynı bellek düzenine sahip (örneğin, hem işaretçi, len ve cap içeren başlık yapıları olan []int ve []string) türler aynı uygulanan makine kodu şablonunu paylaşır. Tür spesifik işlemler için (örneğin, yöntem dağıtımı veya tür doğrulamaları gibi) derleyici, meta verilerinin ofsetlerini içeren gizli bir çalışma zamanı sözlüğü geçirir. Bu, Point{X:1, Y:2} ve Vector{X:1, Y:2}'nin kodu paylaşmasını sağlarken, değer türlerinin yığında kutulanmadan kalmasını sağlar.
Yüksek performanslı bir sütunlu depolama motoru geliştiriyorduk ve hem int64 zaman damgalarını hem de özel Decimal128 yapıları (16 byte, iki uint64 alanı) indekslemek için bir generik SkipList uygulamasına ihtiyaç duyuyorduk. interface{} kullanarak yapılan ilk benchmarklar, toplam CPU zamanının %35'inin çalışma zamanı yığın tahsisatları ve arayüz dolaylılıklarından kaynaklandığını gösteriyordu, bu da alt mikro saniye gecikme gereksinimlerimiz için kabul edilemezdi.
Üç mimari yaklaşım düşündük. İlk olarak, go generate ve text/template aracılığıyla tam monomorfizasyon gerçekleştirmeyi ve özel SkipListInt64 ve SkipListDecimal uygulamaları üretmeyi düşündük. Bu çözüm tahsisatları ortadan kaldırdı ancak on iki farklı sayısal türü desteklerken ikili boyutumuzu 22MB artırdı, bu da sunucusuz dağıtım kısıtlamalarımızı ihlal etti. İkinci olarak, bellek yönetimini elle yönetmek için unsafe.Pointer ve yansımayı kullanarak birleşik bir uygulama düşündük. Bu, ikili boyutu minimal tuttu ancak manuel işaretçi aritmetiği gerektirdiğinden felaket bir karmaşıklık getirdi ve test sırasında Go'nun çöp toplayıcısının değişmezliklerini bozdu.
Üçüncü yaklaşımı seçtik: GC şekil gruplamasına dikkat ederek doğal Go generikleri. Decimal128 yapımızı, [2]uint64'in bellek düzeniyle eşleşecek şekilde hizaladık, böylece diğer 16 byte değer türleri ile şablon kodunu paylaştık. Derleyici çıktısını go tool objdump ile analiz ederek SkipList[int64] ve SkipList[uint64]'nin aynı montaj bloklarını paylaştığını doğruladık, bu arada SkipList[string] ilgili işaretçi içeren bitmap nedeniyle doğru bir şekilde ayrı bir şablon kullandı. Bu hibrit yaklaşım, kod üretimine kıyasla ikili boyutu %58 oranında azaltırken sıfır tahsisat verimliliğini korudu. Sonuç, interface{} versiyonuna göre 4 kat gecikme iyileştirmesi ve 30MB'ın altında bir ikili boyut oldu.
Aynı alan türlerine sahip iki farklı yapı türü neden bazen ayrı generik örnekleri üretirken, bir yapı ve bir ilkelin tür taklidi kodu paylaşabilir?
Bu, GC şekil gruplamasının tamamlayıcı çalışma zamanı tür tanımlayıcısına—işaretçi bitmapleri ve doldurma dahil—dayandığı için olur, yalnızca yüzeysel alan türlerine değil. Eğer type A struct { x, y int } ve type B struct { x, y int } farklı paketlerde tanımlandıysa, aynı GC şekil ve şablonu paylaşırlar. Ancak, *type C struct { x int; y int } farklı bir işaretçi bitmapine sahip olduğundan, type D struct { x, y int } ile ayrı makine kodunun üretilmesine neden olur. Öte yandan, type MyInt int ve int şekilleri paylaşırken, struct { _ int; x int } ve struct { x int } konumlandırma doldurma nedeniyle farklı olabilir. Çöp toplayıcının her canlı değişken için doğru yığın haritalarına ihtiyaç duyduğunu anlamak, düzen kimliğinin nominal tür kimliğinden daha önemli olduğunu açıklar.
Generik tür parametrelerinde yöntem dağıtımı, doğrudan somut çağrılardan nasıl farklıdır ve bu yükün tamamen monomorfizasyonsuz neden kaçınılmaz olduğunu açıkla?
Generik bir tür parametresi T üzerindeki bir yöntemi çağırırken, derleyici doğrudan bir fonksiyon adresi yerine çalışma zamanı sözlüğü aracılığıyla dolaylı bir çağrı yapar. Arayüz çağrılarının—yöntemleri çalışma zamanı boyunca itab aracılığıyla çözmesine benzer şekilde—generik sözlük girişleri derleme zamanında çözülür ancak gizli parametreler olarak geçirilir. Bu, sıfır maliyetli monomorfize koduna kıyasla (genellikle 2-5 nanosecond) bir dolaylılık düzeyi getirir. Adaylar genellikle generiklerin elle uzmanlaşmış koda göre tamamen sıfır maliyetli olduğunu varsayarlar; gerçekte, sözlük araması bazı inlining optimizasyonlarını engeller ki bu monomorfizasyonun mümkün kıldığı bir şeydir, ancak bu reflect.Value.Call'dan çok daha hızlı kalır.
Boş tanımlayıcı alan (örneğin, struct { _ int64; x int64 }) ile bir generik türü örneklemenin, derleyiciye ayrı bir şablon üretme zorunluluğu getirmesi ve ikili boyutu artırmasının nedenleri nedir?
Boş alanlar yer kaplar ve isimlendirilmemiş olsalar bile yapının işaretçi bitmapine katkıda bulunurlar, bu da GC şeklini değiştirebilir. struct { _ int64; x int64 }, bazı mimarilerde struct { x int64 }'den farklı bir boyut ve hizalamaya sahip olacağından, derleyici bunun için farklı bir şablon grubuna atama yapar. Ayrıca, eğer boş alan bir işaretçi türündeyse (**_ int*), bu tür için çöp toplayıcının izleme gereksinimlerini değiştirir ve ayrı yığın haritalarını zorunlu kılar. İkili boyutu optimize etmeye çalışan geliştiricilerin, GC şekillerinin bellek düzeninin tamamına ve dolgu ile boş alanlara göre belirlendiğini anlamaları gerekmektedir.