GoProgramlamaGo Geliştirici

**Go**'da bir yöntem değeri oluşturulurken bir yığın (stack) üzerinde tahsis edilen bir değerin **heap**'e hangi koşullar altında örtük olarak yükseltildiği ve sonuçta ortaya çıkan kapanışı temsil eden iç yapının ne olduğu?

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

Sorunun yanıtı.

Soru geçmişi

Yöntem değerleri, yöntemleri birinci sınıf işlevler olarak ele almanın sorunsuz bir yolunu sağlamak için Go'nun erken sürümlerinde tanıtılmıştır ve bu, Go'nun basitlik ve sözel kapsam üzerindeki vurgusuyla uyumludur. Bu özellikten önce, geliştiricilerin alıcıyı açıkça yakalayan işlev liteleri kullanarak kapanışları manuel olarak oluşturmaları gerekiyordu, bu da fazla sözdizimsel tekrar gerektiriyordu. Mevcut uygulama, f := obj.Method gibi ifadelerin bağlı bir işlev oluşturmasına izin verir, ancak bu kolaylık Go'nun kaçış analizi ve bellek modeli ile ince etkileşimler sağlar.

###Problem obj bir yığın üzerinde saklanan bir değer türü olduğunda ve Method bir işaretçi alıcısı ( func (t *T) Method(...)) olarak belirtilmişse, derleyici, alıcının döndürülen işlev değerinin ömrü için geçerli kalmasını sağlamalıdır. Yöntem değeri heap'e kaçabilecek olduğundan -örneğin, bir channel içinde saklandığında, bir global değişkene atandığında veya yeni bir goroutine içinde başlatıldığında- derleyici orijinal yığın çerçevesinin hayatta kalıp kalamayacağını garanti edemez. Sonuç olarak, derleyici değeri bir işaretçiye (&obj) örtük olarak dönüştürür, bu da alıcının heap-tahsis edilmesini tetikleyerek görünmez bir tahsis sıcak noktasının oluşmasına yol açar, bu da GC baskısını etkiler.

Çözüm

zaman aşımı yöntemi değeri, gerçek yöntem koduna bir işaretçi ve alıcının heap adresini tutan bir veri kelimesini içeren bir closure (bir func value yapısı) olarak temsil eder. Bu, üretilen thunk'ın kapanışın nerede yol aldığına bakılmaksızın doğru bağlamla yöntemi çağırmasını sağlar. Bu tahsisi önlemek için, geliştiriciler ya işlev ifadeleri (T.Method veya (*T).Method) kullanabilir ve alıcıyı açıkça geçirerek çağıranın ömrünü kontrol etmesini sağlayabilir veya bağlamadan önce orijinal değerin zaten heap-tahsis edildiğinden emin olabilir (örneğin, new(T) veya &T{} yoluyla).

type Processor struct{ data []byte } func (p *Processor) Process() { /* ... */ } func main() { // Yığın üzerinde tahsis edilen değer var p Processor // Örtük: &p, kapanışı oluşturmak için heap'e kaçar f := p.Process // Tahsis burada gerçekleşir go f() // Kapanış başka bir goroutine'de kullanılır }

Gerçek hayattan bir durum

Ekibimiz, gelen her piyasa veri paketinin bir geri çağırma kaydı tetiklediği yüksek frekanslı bir ticaret kapısı geliştirdi. Mimaride, handler := adapter.HandlePacket ifadesi, yerel bir Adapter yapısındaki işaretçi-alıcı yöntemine bağlı bir yöntem değeri oluşturmak için bir dağıtıcı kalıbı kullanıldı. Yük profilleme altında, bu yöntem değeri yapılandırmalarından kaynaklanan runtime.newobject'ta aşırı tahsislere tanık olduk ve bu da gecikme SLA'mızın ihlaline neden olan GC duraklamalarına yol açtı.

Bu durumu çözmek için üç farklı yaklaşımı değerlendirdik. İlk olarak, tüm yöntemleri değer alıcılarına dönüştürmeyi değerlendirdik ki bu da heap tahsisini ortadan kaldırdı ancak değişken durum kalıplarımızla tutarlılığı ihlal etti ve her çağrıda büyük yapı kopyalarına yol açtı. İkinci olarak, kapanış tahsisini tamamen ortadan kaldıran yöntemi ifadeleri ile birlikte açık adaptör işaretçileri geçirerek deney yaptık, bu tüm dağıtıcı arayüzünün ek bir bağlam parametresi kabul etmesi gerektiriyordu, bu da geriye dönük uyumluluğu bozdu. Üçüncü olarak, talepler arasında yeniden kullanılan, önceden tahsis edilmiş adaptör işaretçilerinin bir sync.Pool'unu uyguladık, bu da yöntem değerlerinin istikrarlı heap adreslerini yakalamasına izin verdi, böylece talep başına tahsis olmadan çalıştı.

Üçüncü çözümü seçtik çünkü mevcut arayüz sözleşmelerimizi korurken tahsis maliyetini binlerce talep boyunca amorti etti. Sonuç, sıcak yolda talep başına tahsilleri iki (alıcı + kapanış) sıfıra düşürdü ve GC gecikmesini 15 ms'den 2 ms'nin altına düşürdü, piyasa dalgalanma dönemlerinde.

Adayların sıkça kaçırdığı noktalar

Bir değeri interface{}'e dönüştürmek, eğer değer adreslenebilir ise neden bir heap tahsisini de zorlar ve bu yöntem değeri tahsisi ile nasıl farklılık gösterir?

Bir somut değeri bir interface{}'ye atarken, Go hem tip tanımlayıcısını hem de verilere bir işaretçi saklamak zorundadır. Eğer değer yığın üzerinde başlıyorsa, derleyici bir kopyasını heap-tahsis etmek zorundadır çünkü arayüzler yığın çerçevesinden daha uzun süre yaşamayı gerektirebilecek referans benzeri konteynerlerdir. Yöntem değerlerinden farklı olarak - belirli bir yöntem için belirli bir alıcıyı yakalayan - arayüz dönüşümleri yalnızca veri kelimesini ve tip işaretçisini tahsis eder, dinamik yönlendirmeyi destekleyen bir dolayım oluşturarak sözel kapanışı değil, ancak her iki işlem de kaçış analizini tetikler.

Derleyici, bir değer üzerindeki bir yöntem çağrısının alıcılarının kaçıp kaçmadığını belirlerken bir yöntem çağrısını değer üzerinde mi yoksa işaretçi üzerinde mi gerçekleştirdiğini nasıl ayırt eder ve neden masum bir obj.Method() çağrısı tahsis yapabilir?

Derleyici, yönteminin tanımlı alıcı tipini inceleyerek AST içerisinde analiz eder. Eğer yöntem bir işaretçi alıcısına sahipse ve bir değer üzerinde çağrılıyorsa, derleyici örtük olarak bir & işlemi ekler. Eğer çağrı sonucu veya yöntem değeri kendisi kaçıyorsa, alıcı kaçar. Adaylar genellikle doğrudan çağrıların bile tahsis edebileceğini gözden kaçırır çünkü derleyici, işaretçinin dönen değer veya global duruma kaçarak kaçmadığını kanıtlayamazsa, bu durum, derleme zamanında bilinmeyen somut bir türle ilgili arayüz yöntemi çağrılarına değildi.

Bir yöntem değeri kapanışından orijinal alıcı adresini kurtarabilir misiniz ve neden iki yöntem değeri için eşitlik karşılaştırması her zaman yanlış sonuç verir?

Hayır, kapanıştan alıcı adresini yansıma olmadan kurtaramazsınız çünkü func value opak bir runtime yapısıdır. Yöntem değerleri karşılaştırılamaz çünkü kapanış bağlamına gizli bir veri işaretçisi içermektedir ve Go işlev değerlerini nil dışında karşılaştırmayı yasaklamaktadır. Farklı alıcılara bağlı olan iki yöntem değeri, farklı veri işaretçileri ile ayrı kapanışlardır, aynı alıcıya bağlı olan iki yöntem değeri ise yine de ayrı heap-tahsis edilmiş kapanış yapılarına sahiptirler, bu da anlamlı bir eşitliğin belirlenmesini imkansız hale getirir.