Go, bir değişkenin yığında mı yoksa yığıtta mı (heap) mı yer alması gerektiğine karar vermek için derleme sırasında kaçış analizi (escape analysis) kullanır. Bir yerel değişkenin işaretçisi, bildiren fonksiyonu terk ettiğinde – geri dönüş değerleri, küresel değişkenlere atama yapılması veya onu saklayan fonksiyonlara gönderilmesi durumunda – derleyici, onu yığın tahsisi için işaretler. Bu, bellek güvenliğini sağlar çünkü yığın çerçevesi fonksiyon geri döndüğünde yok edilirken, yığın GC tarafından yönetilir. Analiz, değişken referanslarının bir grafiğini oluşturur ve fonksiyon sona erdikten sonra erişilebilecek herhangi bir düğümü transitif olarak işaretler. Sonuç olarak, yerel bir yapı için işaretçi döndürmek gibi görünüşte masum bir kod, yığın tahsisine neden olurken, yapının değerini kopyalayarak döndürmek yığının yeniden kullanımına olanak tanır.
Yüksek frekanslı ticaret kapımızda kritik bir performans gerilemesiyle karşılaştık; profil oluşturma, bir yardımcı fonksiyonun yığında her saniye binlerce küçük yapı tahsis ettiğini ortaya koydu. Fonksiyon, kopyalama yükünü en aza indirmek için *OrderInfo işaretçileri döndürdü, bu da Go'nun kaçış analizini tetikleyerek bu değişkenlerin yığından yığıta (heap) yükseltilmesine neden oldu. Bu, toplamCPU zamanının yüzde otuzunu tüketen aşırı GC döngüleri üretti ve kullanım durumumuz için kabul edilemez düzeyde mikro saniye gecikmelerine yol açtı.
Kodu işaretçileri yerine değerleri döndürecek şekilde yeniden yapılandırmak, tahsisi tamamen ortadan kaldırırdı, çünkü veriler çağıranın yığın çerçevesinde kalacak ve geri dönerken otomatik olarak serbest bırakılacaktı. Ancak, kilometre taşları, bu yaklaşımın kopyalama yükü nedeniyle gecikmeyi yaklaşık yüzde beş artırdığını gösterdi, bu da sıkı gerçek zamanlı performans SLA'mıza aykırıydı ve bu nedenle reddedildi.
sync.Pool uygulamak, talepler arasında yeniden kullanılmak üzere önceden tahsis edilmiş OrderInfo nesnelerini saklayarak cazip bir ortalama sundu. Bu strateji, tahsis oranlarını ve GC duraklama sürelerini düşürerek kopyalama cezası olmadan işaretçi tabanlı API sözleşmesini korudu. Ana komplikasyon, yeniden kullanım öncesinde havuzlanan nesneleri temizlemek için titiz bir sıfırlama mantığını uygulamaktı; bu, sıradaki talepler arasında hassas ticaret verilerinin sızmasını önler.
Siparişleri gruplar halinde işlemek, birden fazla işlem boyunca tahsis maliyetlerini yaymak anlamına gelirdi. Bu yaklaşım, işlem başına yükü önemli ölçüde azaltmasına rağmen, tamponlama gecikmeleri kabul edilemez gecikmelere yol açtığı için bireysel ticaretler için uygun değildi.
Sonuç olarak, sync.Pool'u en iyi çözüm olarak seçtik çünkü bu, bellek verimliliği ile platformun alt mikro saniye gecikme gereksinimleri arasında bir denge kurdu. Üretime geçişten sonra, GC yükü toplam CPU kullanımının yüzde ikiye düştü ve p99 gecikmesi gerekli eşiklerin çok altında stabilize oldu ve aynı zamanda verimliliği korudu.
Yerel bir işaretçiyi interface{}'ye atamak, arayüz hemen atılsa bile neden yığın tahsisini zorlar?
Bir işaretçi interface{}'ye atandığında, Go çalışma zamanı, hem tür tanımlayıcısını hem de veri adresini içeren dahili bir kalın işaretçi (fat pointer) inşa etmelidir. Go'da arayüzler çalışma zamanı yapılarına işaretçi olarak uygulandığından, derleyici, altındaki verilerin, arayüz değeri aracılığıyla fonksiyondan daha uzun ömürlü olmayacağını kanıtlayamaz. Sonuç olarak, Go, işaretçinin kendisi kaçsa bile güvenliği sağlamak için işaret edilen belleği yığıta (heap) yükseltir. Bu davranış, yerel arayüz kullanımının somut değer için yığın tahsisatı garantilediğini varsayan geliştiricileri sıkça şaşırtır.
Bir döngü değişkeninin bir kapamada (closure) yakalanması, o değişken için kaçış analizini nasıl etkiler?
Go 1.22'den önce, döngü değişkenleri bir kez tahsis edilir ve yinelemeler (iterations) boyunca yeniden kullanılır; bu, bunları yakalayan kapamaların hepsinin aynı yığında tahsis edilen bellek adresini referans alacağı anlamına gelir. Bir kapama fonksiyonu terk ettiğinde – örneğin bir goroutine'e gönderildiğinde veya döndürüldüğünde – derleyici, yakalanan değişkenin geçerliliğini sağlamak için onu yığıtta tahsis etmek zorundadır. Dil değişikliğiyle yineleme başına tahsise geçiş olmasına rağmen, kaçış analizi, kapamanın ömrünün üstteki yığın çerçevesi tarafından sınırlı olduğunu kanıtlayamadığında kapama yakalamalarını ihtiyatlı bir şekilde ele almaya devam eder. Adaylar genellikle kapama yakalamanın yığın tahsisatı üzerinde işaretçi yarattığını ve yığın yerine tahsisi zorladığını gözden kaçırır.
Bir dilim (slice) değer döndürüldüğünde, derleyici neden dilimin arka dizi (backing array) tahsisatını yığında yapabilir?
Bir dilimi değer olarak döndürmek, yalnızca dilim başlığını - gösterici, uzunluk ve kapasite içeren - kopyalar, verilerin bulunduğu dizi değil. Eğer arka dizi yığında tahsis edilmişse, fonksiyon geri döndüğünde geçersiz hale gelir ve döndürülen dilim başlığı kullanılmayan belleğe işaret eder. Bu nedenle, Go'nun kaçış analizi, dilim başlığının kendisi fonksiyondan kaçarak otomatik olarak herhangi bir dilim arka dizisini yığıta (heap) yükseltir; bu durumda başlık hafif bir değer türüdür. Geliştiriciler genellikle dilim başlığının yığın tahsisatını, arka verilerin yığın tahsisatıyla karıştırır ve dizinin geçerliliğini sağlamak için fonksiyon kapsamını aşması gerektiğini gözden kaçırır.