GoProgramlamaGo Arka Uç Mühendisi

Go'nun iş parçacığına özel ayırıcılarının küçük nesne isteklerini küresel bir kilit almadan nasıl karşılayabileceğini garanti eden invarianta nedir?

Hintsage yapay zeka asistanı ile mülakatları geçin

Sorunun Cevabı

Tarihçe

Go'nun bellek ayırıcısı, C++ çok iş parçacıklı sunucular için tasarlanmış TCMalloc, Google'ın iş parçacığı önbellekli malloc'undan türemiştir. Çalışma zamanı, eşzamanlı programlarda kilit rekabetini ortadan kaldırmak için özel olarak çok seviyeli bir önbellek uygular. Bu tasarım, küçük nesne hızlı yolunda bellek verimliliğinden daha fazla verimlilik önceliği taşır.

Sorun

Son derece eşzamanlı hizmetlerde, her tahsisin küresel bir yığın kilidi almasını gerektirmek, goroutinelerin seri hale gelmesine ve verimliliğin düşmesine neden olur. Zorluk, yaygın durumda senkronizasyon olmadan O(1) tahsis gecikmesi sağlamaktır ve güvenliği sürdürmektir. Geleneksel malloc uygulamaları, birden fazla CPU'nun aynı kilit kelimesi için rekabet etmesi sırasında önbellek hatlarının kayması gibi sorunlarla karşılaşır.

Çözüm

Çalışma zamanı, 67 boyut sınıfı için her bir P ye ait bir önbellek (mcache) tutar. Bir goroutine küçük bir nesne tahsis ettiğinde (≤32KB), ya bir sınır işaretçisini artırır ya da mcache içindeki iş parçacığına özel boş listeyi kullanarak çıkarır; bu da atomik işlemler gerektirmez. Kritik invariant, bir mcache'in anlık olarak yalnızca bir P'ye ait olması ve tahsislerin asla P sınırlarını aşmaması gerektiğidir; böylece paylaşılan değişken durumdan kaçınılır.

type PriceTick struct { Symbol uint32 Price float64 } func ProcessTick() { // Kilitlenmeden mcache'den 16 byte tahsis eder tick := &PriceTick{} _ = tick }

Hayattan Bir Durum

Yüksek frekanslı bir ticaret platformu, fiyat normalizasyonu için her bir olayın geçici 24 baytlık yapılarına ihtiyaç duyduğu sırada, saniyede 500.000 piyasa veri olayı işledi. İlk uygulama, bu nesneler için küresel bir sync.Pool kullandı ve bu yük altında ciddi bir rekabet noktasına dönüştü ve atomik işlemler ve önbellek tutarlılığı trafiği için CPU zamanının %35'ini tüketti.

Çözüm A: Manuel Havuz Bölme

Ekip, havuzu goroutine ID hash ile seçilen 256 içsel alt havuza manuel olarak bölmeyi düşündü. Artılar: Rekabeti önbellek hatları arasında dağıtır. Eksiler: Düzensiz kullanım, boş kalan bölümlerde bellek şişkinliğine yol açar ve diğerlerinde boş nesneler varken yerel bir bölüm boşaldığında karmaşık açlık yönetimi gerektirir.

Çözüm B: İşçi Alanları

Büyük bellek alanlarının her işçi goroutine için önceden tahsis edilmesini ve artış işaretçili tahsisi değerlendirildi. Artılar: Hiçbir rekabet ve son derece hızlı tahsis yolu. Eksiler: Manuel bellek yönetimi gerektirir, sıfırlama işaretçileri yanlış yönetilirse bellek sızıntısı riski taşır ve asenkron sınırlarda nesne ömrü takibini karmaşıklaştırır.

Çözüm C: Yığın Tahsisi ve Gruplama

Seçilen yaklaşım, olayı işleme işleyicisini olabildiğince yığın üzerinde veri tutacak şekilde yeniden yapılandırarak, mümkünse değer yapıları yerine işaretçileri kullanarak ve tahsisi amortize etmek için olayları 1000’lik gruplar halinde işleyerek tasarlandı. Artılar: Kısa ömürlü veriler için yığın basıncını tamamen ortadan kaldırır ve senkronizasyon primitifleri gerektirmez. Eksiler: Daha önce işaretçi anlamsını bekleyen arayüzlerin önemli bir yeniden yapılandırmasını talep eder ve her goroutine için yığın kullanımını artırır.

Sonuç

Çözüm C'yi uygulayarak, hizmet sıcak yol üzerindeki yığın tahsislerinin %99'unu ortadan kaldırdı. P99 gecikmesi 12 milisaniyeden 180 mikro saniyeye düştü ve çöp toplama döngüleri %85 oranında azaldı, bu da hizmetin alt milisaniye SLA gereksinimlerini karşılamasını sağladı.

Adayların Sıklıkla Atladığı Şeyler

Go, sabit boyutlu spanlardan farklı boyutlarda nesneler tahsis ederken bellek parçalanmasını nasıl sınırlar?

Go, belirli bir granülariteye sahip 67 farklı boyut sınıfı kullanır (8 bayt adımlarla 512 bayta kadar, sonra daha büyük aralıklar). Nesneler, iç parçalanmayı yaklaşık %12,5 ile sınırlamak için en yakın sınıf boyutuna yuvarlanır. Dış parçalanma minimize edilir çünkü her mspan, tam olarak bir boyut sınıfına ait nesneleri içerir, bu nedenle küçük nesneler büyük bellek bloklarını sıkıştırmaz.

Çalışma zamanı neden ayırma sırasında kullanıcıya görünür bellek yerine yığın bit haritalarını temizler?

Ayırıcı, nesne başlıkları yerine heapArena meta veri yapılarına tür bilgilerini ve işaretçi bit haritalarını tutar. Bellek tahsis edildiğinde, yalnızca gerektiğinde işaretçi boşluklarını gösteren bit haritaları sıfırlanır; veri belleği, mutatör veya eşzamanlı temizleme sırasında gereksinim üzerine sıfırlanır. Bu yaklaşım, işleri ertelemekten, önbellek yerelliğini iyileştirmekten ve tahsis sırasında gereken bellek bant genişliğini azaltmaktan fayda sağlar.

Bir span'ın çöp toplamada mcache'den mcentral'e geçiş yapmasını zorlayan nedir?

GC temizleme aşaması sırasında, çalışma zamanı mcache örneklerinde tutulan spanları inceler. Eğer bir span, tahsis edilmiş nesne içermezse (tüm boşluklar serbest), P onu mcentral'e geri verir; bu da bellek tutumunu önler ve serbest belleğin dengeli dağılımını sağlar, ancak merkezi kilidi yeniden edinme maliyetini de üstlenir.