Sorunun cevabı.
Go 1.20'den önce, derleyici arayüz yönlendirmelerini optimize etmek için tamamen statik sezgilere dayanıyordu; bu, doğal olarak dolaylıdır ve iç içe geçmeyi engeller. PGO'nun tanıtılması, optimizasyon aracını geri bildirim yönlendirmeli optimizasyona kaydırdı, bu da araç zincirinin gerçek dünya yürütme izlerini kullanarak sıcak arayüz çağrı alanlarını spekülatif olarak monomorfikleştirmesine olanak tanıdı.
Go'daki arayüz değerleri bir tür tanımlayıcı (itable) ve bir veri işaretçisi taşır. Her yöntem çağrısı, somut işlev işaretçisini bulmak için itable'ı derefere etmeyi gerektirir; bu, iç içe geçeni genişletmekten alıkoyar ve kaçış analizini belirsiz hale getirir. Yüksek verimli kod yollarında (ör. io.Reader zincirleri) bu dinamik yönlendirme yükü, CPU döngülerinin %10–15'ini tüketebilir, yine de derleyici belirli bir çağrı alanında hangi somut türlerin hakim olduğunu statik olarak kanıtlayamaz.
Derleyici, temsili bir iş yükünden toplanan bir CPU profili (pprof) alır. Çağrı alanlarının kenar ağırlıklarını hesaplar; eğer belirli bir arayüz çağrısı %90'ın üzerinde bir örnekle tek bir somut türe çözülüyorsa (varsayılan eşik), arka uç itable işaretçisini hashlenmiş tür kimliği ile karşılaştıran bir koruma kontrolü yayınlar. Eğer koruma başarılı olursa, yürütme doğrudan bir çağrıya akar (iç içe geçebilir); aksi takdirde, standart dolaylı yönlendirmeye geri döner. Faydalanmak için, ikili dosya -pgo=<file> bayrağı ile inşa edilmelidir; burada <file>, runtime/pprof veya test paketinden üretilen geçerli bir CPU profilidir.
// Abstraksiyon kullanan hizmet katmanı tip Processor arayüzü{ Process([]byte) hata } tip Görev yapı{ handler İşlemci } func (t *Görev) Çalıştır(data []byte) hata { // PGO olmadan: itable araması aracılığıyla dolaylı çağrı // PGO ile: Eğer t.handler, profillerin %99'unda *JSONProcessor ise, // derleyici şu kodu ekler: // if t.handler.(*JSONProcessor) != nil { JSONProcessor.Process'ı doğrudan çağır } return t.handler.Process(data) }
Hayattan bir durum
Telemetry hattımız, interface{} tabanlı bir eklenti mimarisi kullanarak saniyede milyonlarca olayı ayrıştırdı. Profil oluşturma, CPU zamanının %18'inin runtime.convT2E ve Parser arayüzü içindeki dolaylı çağrı yüküne harcandığını ortaya koydu. Üç düzeltme stratejisi için değerlendirmelerde bulunduk.
Çözüm 1: Tür geçişleri ile manuel tür teyitleri. Arayüzü, her çağrı alanında somut bir tür kontrolü ile değiştirebilirdik. Artılar: Kesin olarak sıfır maliyetli yönlendirme ve derin iç içe geçme. Eksiler: İş mantığını altyapı endişeleriyle kirletiyor, eklenti soyutlamasını kırıyor ve her yeni ayrıştırıcı varyant eklendiğinde birçok çağrı alanının güncellenmesini gerektiriyordu.
Çözüm 2: Genel türlere geçiş. Parser'ı bir tür parametresi Parser[T herhangi] olacak şekilde dönüştürmek, derleme zamanında monomorfikleştirme sağlayacaktı. Artılar: Tür güvencesi ve çalışma zamanı kontrolleri olmadan sıfır yük. Eksiler: Arayüz, hala dinamik bağlantı ve çalışma zamanı eklenti kaydı yapan dış ekipler tarafından kullanılan paylaşılan bir kütüphanede tanımlanmıştı; genel türler, tüm modüllerin statik yeniden derlenmesi olmadan eklenti sınırını geçemez.
Çözüm 3: PGO'yu etkinleştirme. Üretim kanaryamızdan pik yük altında 30 saniyelik bir CPU profili topladık ve CI/CD derleme hattımıza -pgo=prod.pprof ekledik. Artılar: Kaynak kodunda sıfır değişiklik, sıcak yolların otomatik optimizasyonu ve soğuk yollar için nazik bir degradasyon. Eksiler: Profil alımından dolayı derleme süresi %12 arttı ve trafik desenleri geliştikçe profilleri yenilemek için tekrar eden bir iş kurmamız gerekti.
Çözüm 3'ü kabul ettik. Ortaya çıkan ikili dosya, de sanallaştırılmış yolların önceki yığın tahsisatına izin verdiği için p99 gecikmesinde %14 azalma ve bellek tahsislerinde %9 azalma gösterdi. Profili, otomatik kanarya dağıtımları yoluyla haftalık olarak yeniledik.
Adayların sıkça gözden kaçırdığı şeyler
Eğer profil bayat veya temsil edici değilse, PGO programın gözlemlenebilir davranışını veya doğruluğunu değiştirebilir mi?
Hayır. PGO optimizasyonları kesinlikle spekülatiftir. Derleyici her zaman standart arayüz yönlendirmesini gerçekleştiren bir geri dönüş yolu yayınlayarak orijinal anlamı korur. Eğer profil yanlış somut türü tahmin ederse, koruma başarısız olur ve yürütme yavaş yol üzerinden güvenli bir şekilde devam eder. Performans, PGO olmayan temel düzeye geri dönebilir, ancak program panik yapmaz veya yanlış sonuçlar üretmez.
PGO, soğuk yol için kod üretimi açısından manuel tür teyitlerinden nasıl farklıdır?
Manuel tür teyitleri (if somut, tamam := iface.(Tür); tamam) tek bir statik varsayım kodlamaktadır. Eğer teyit başarısız olursa, programcı hatayı ya da panik durumunu ele almak zorundadır. Ancak PGO, sıcak tür için bir tür kontrol koruması ve ardından doğrudan bir çağrı üretir, ancak otomatik olarak tüm diğer türler için orijinal arayüz çağrısına bağlanır. Bu "polimorfik içe geçirme önbelleği" tarzı, optimize edilmiş ikili dosyanın kaynak kodu dallanması olmadan birden fazla somut türü nazikçe ele almasını sağlar; oysa manuel teyitler tek bir türü katı bir şekilde zorlar.
Çerçeve işaretçileri etkinleştirilen bir ikili dosyadan toplanan CPU profilinin kritik olmasının sebebi nedir ve çerçeve işaretçilerinin yokluğu PGO etkinliğini nasıl kötüleştirir?
Go çalışma zamanı, profilleme sırasında yığınları açarak örnekleri kaynak satırlarına atfeder. Çerçeve işaretçileri (çoğu mimaride Go 1.21’den itibaren varsayılan olarak etkinleştirilmiştir) bu açılmayı hassas ve hızlı hale getirir. Onlarsız, profilleyici, yanlış çağrı alanlarına veya kısa işlevleri tamamen atlayarak örnekleri yanlış atfetmek için sezgiler veya dwarf meta verileri kullanmak zorundadır. Bu gürültü, kenar ağırlık hesaplamalarının doğruluğunu azaltır ve bu, derleyicinin sıcak arayüz çağrılarını kaçırmasına veya soğuk olanları optimize etmesine neden olarak de sanallaştırmanın performans kazanımlarını zayıflatır.