Go 1.14'ten önce, derleyici her defer ifadesi için yığın üzerinde _defer yapısını yığın üzerinde tahsis ediyor ve bunu her goroutine için bir bağlı listeye bağlıyordu. Bu önemli bir GC baskısı oluşturdu ve derinlemesine iç içe geçmiş defer'ler için O(n) maliyet getirdi.
Go 1.14, yığın üzerinde tahsis edilen defer'ler getirdi ve bu da derleyicinin _defer yapılarını doğrudan işlevin yığın çerçevesine yerleştirmesine olanak tanıdı, kaçış analizi onların işlevden daha uzun sürmediğini kanıtladığında. Daha sonraki sürümler açık kodlu defer'ler (Go 1.17+) ekledi, burada derleyici temizleme kodunu doğrudan işlev epiloguna yerleştiriyor, bu da çalışma zamanı çağrılarını kullanmaktan kaçınıyor.
Panik kurtarma sırasında, çalışma zamanı yığın çerçevesini çerçeve çerçeve tersine çözüyor. Aktif çerçevelerde bulunan yığın üzerinde tahsis edilen defer'leri yürütüyor, ardından bağlı listeden kalan yığın üzerinde tahsis edilen defer'leri çalıştırıyor. Bu hibrit yaklaşım, katı LIFO sırasını korurken yaygın durumda tahsis maliyetini ortadan kaldırıyor.
Bir Go ile yazılmış yüksek frekanslı ticaret API sarmalayıcısı, piyasa dalgalanması sırasında 200 milisaniye GC duraklaması yaşıyordu.
Ekip, sorunun aşırı yığın tahsisinden kaynaklandığını tespit etti. Her HTTP istek yöneticisi, tx.Rollback() ve bağlantı temizliği için birden fazla defer ifadesi kullanıyordu. Yük altında, bu saniyede milyonlarca _defer yapısı oluşturuyordu ve sık sık nesne toplama döngüleri tetikleniyordu.
Çözüm A: Manuel kaynak yönetimi. Ekip, tüm defer çağrılarını kaldırmayı ve her dönüş noktasında açık bir Close() ve Rollback() kullanmayı düşündü. Artıları: Sıfır tahsis maliyeti ve öngörülebilir performans. Eksileri: Kod kırılgan hale geldi ve hata yapmaya eğilimli oldu, birden fazla çıkış yolu arasında tekrarlayan temizleme mantığıyla sonuçlandı.
Çözüm B: Nesne havuzlama. Veritabanı işlem nesnelerini havuzlamayı denediler. Artıları: Kullanıcı kodunda tahsisleri azalttı. Eksileri: Bu, _defer yapısı tahsislerini çözmedi, çünkü bunlar çalışma zamanı içindeydi ve kullanıcı kodu tarafından havuzlanamazdı.
Çözüm C: Derleyici yükseltmesi ve yeniden yapılandırma. Ekip, Go 1.13'ten 1.18'e yükseltti ve yığın üzerinde kaçış yapan değişkenleri yakalamaktan kaçınmak için kapanımları yeniden yapılandırdı. Artıları: Otomatik yığın tahsisi ve çoğu durumda sıfır çalışma zamanı maliyetiyle açık kodlama defer'ları. Eksileri: Panik kurtarma davranışının doğru kaldığını doğrulamak için kapsamlı gerileme testleri gerektirdi.
Çözüm C'yi seçtiler. Yaygın kullanım sonrası, GC duraklama süreleri alt milisaniyeye düştü ve istek verimliliği iş mantığında herhangi bir değişiklik olmaksızın %40 arttı.
İsimlendirilmiş bir dönüş parametresini değiştiren bir işlevin ertelenmesi, döndürülen nihai değeri nasıl etkiler ve bu desen isimsiz dönüşlerde ne zaman başarısız olur?
Bir Go işlevinde isimlendirilmiş dönüş değerleri (örneğin, func f() (err error)) kullanıldığında, ertelenmiş işlev bu dönüş parametresinin gerçek yığın alanını kaplar. defer içinde o isimle yapılan her atama, çağırana döndürülmek üzere değiştirilen değeri etkiler. İsimsiz dönüşlerde, dönüş değeri geçici bir kayıt veya yığın konumuna kopyalanmadan önce ertelenmiş işlevler çalıştırılır, bu da defer içindeki değişikliklerin çağırana görünmez olmasını sağlar. Adaylar genellikle deferin, isimlendirilmiş sonuçların nihai değerini işlevin gerçek çıkış anında gördüğünü, defer kaydı anında değil, kaçırır.
Eski Go sürümlerinde sıkı bir döngü içindeki ertelenmiş işlevlerin O(n²) performans karakteristikleri göstermesine ne neden olur ve yığın tahsisi bu maliyeti tamamen ortadan kaldırmaz?
Go sürümleri 1.14'ten önce, for döngüsü içinde defer yerleştirildiğinde, her yinelemede bir yığın nesne tahsis edilir ve bu bağlı listeye eklenir. Bu, liste her iterasyonla doğrusal olarak büyüdükçe, kare karmaşıklık yaratır. Go 1.14+ bunları yığında tahsis etse de, çalışma zamanı bu defleri işlev çıkışında ters sırayla çözmek zorundadır. Bir işlev n işlemi def ederse, çıkış yolu bunları işlemek için O(n) zaman gerektirir. Adaylar genellikle döngüler içinde defer'lemeyi yığın tahsisi ile bile bir anti-durum olarak görmeyi unuturlar; manuel temizleme, işlev kapsamındaki O(n) toplama yerine her yineleme için O(1) maliyet sağlar.
Panik kurtarma ve ertelenmiş işlevler arasındaki etkileşim, nasıl bir ertelenmiş çağrının devam etmesini engeller ve bu, sıralı yürütmeden ne ile ayrılır?
Bir Go işlevi panik yaptığında, çalışma zamanı yığın yapısını çözüyor ve ertelenmiş işlevleri sıralı olarak çağırıyor. Eğer bir ertelenmiş işlev tekrar panik yaparsa ve buna karşılık gelen bir recover() yoksa, o yeni panik orijinal panik değerini değiştirir. Kritik olarak, bir panik, ertelemiş işlevden yukarı akmaya başladığında, çalışma zamanı belirli bir çerçevedeki tüm kalan defer'leri çalıştırmayı durdurur ve yukarıya doğru çözmeye devam eder. Adaylar genellikle defer'lerin işlemsel olmadığını, eğer bir sonraki defer panik yaparsa etkileri geri almadığını ve defer içinde bir panik olursa o çerçevedeki defer zincirinin kalan kısmının kesileceğini ve daha sonraki defer'lerin kritik temizliği yapması gerekiyorsa kaynakların sızmasına neden olabileceğini gözden kaçırır.