Tarihçe. Swift, Objective-C'den ARC'yi miras aldı; burada bloklar (closure'lar) asenkron bağlamlarda güvenliği sağlamak için yakalamaları yığın tahsis eder. Erken Swift sürümleri (1.x–2.x) sınırlı ömrü belirtmek için açık @noescape açıklamalarını gerektiriyordu. Swift 3.0 ile dil bu varsayılan durumu tersine çevirdi: closure'lar varsayılan olarak kaçarak işlemeye başladı ve yığın bağlantılı referanslar için açık @escaping gerektiriyordu. Bu kayma, geliştirici müdahalesi olmadan yığın tahsisi gerektiren bağlamları yığın tahsis edilebilirlerden ayırt etmek için sağlam bir derleme zamanı mekanizması gerektiriyordu.
Sorun. Bir closure, kapsayan alanından değişkenleri yakaladığında, Swift bu yakalanan değerlerin tanımlayıcı fonksiyonun yığın çerçevesinden daha uzun süre yaşayıp yaşamayacağını belirlemek zorundadır. Eğer closure, bir özellikte saklanıyorsa, fonksiyondan dönüyorsa veya asenkron bir işleme iletiliyorsa, yakalamaların bitmeyen gösterimlere (dangling pointers) yol açmaması için yığın tahsis edilmesi gerekir. Ancak, yığın tahsisi senkronizasyon (ARC atomik işlemleri) ve bellek baskısı açısından önemli performans maliyetleri doğurur. Statik analiz olmadan derleyici, tüm closure'ları ihtiyatla yığın tahsis etmek zorunda kalır ve bu da sık döngülerde veya map veya filter gibi fonksiyonel programlama desenlerinde performansı kötüleştirir.
Çözüm. Swift, zorunlu performans optimizasyon geçişleri sırasında SIL (Swift Intermediate Language) seviyesinde kaçış analizini kullanır. Derleyici, closure değerlerinin ve yakalamalarının ömrünü izleyen bir veri akış grafiği oluşturur. Analiz, closure değerinin, çağıranın alanından daha uzun süre devam etmediğini kanıtlıyorsa—küresel duruma kaçış yok, self'de saklama yok, asenkron tutma yok—derleyici closure bağlamını yığın tahsis edilmiş olarak işaretler. Üretilen LLVM IR, closure bağlam yapısı için malloc yerine alloca kullanır ve temizleme, ARC serbest bırakma çağrıları yerine yığın işaretçisi geri yüklemesi ile gerçekleşir. Bu optimizasyon, kaçışmayan işlev parametreleri ve yerel closure'lar için otomatik olarak gerçekleşir, bellek baskısını ve tahsis aşırı yüklenmesini azaltır.
Bir müzik prodüksiyon uygulaması için Swift ile gerçek zamanlı bir ses işleme motorunu optimize ediyorsunuz. DSP boru hattı, buffer parçalarına 16 ardışık filtre uygular ve fonksiyonel zincirleme kullanır:
buffer.applyFilter { $0 * coefficient } .normalize() .clip()
Profiling, CPU zamanının %40'ının closure bağlamlarındaki malloc ve retain çağrılarında harcandığını ve bu durumun 96kHz örnekleme hızlarında ses kesintilerine yol açtığını ortaya koyuyor.
Çözüm A: Tüm fonksiyonel zincirlemeyi zorlayıcı for döngüleri ve manuel dizi dizinlemeleri ile değiştirin.
Artılar: Closure'ları tamamen ortadan kaldırır, yalnızca yığın işlemlerini garanti eder ve tahmin edilebilir performans sağlar.
Eksiler: Kod okunamaz ve sürdürülemez hale gelir; Swift'in standart kütüphane algoritmalarının ifadesel gücünü kaybeder ve hata yüzeyini artırır.
Çözüm B: İşlemi @inline(never) ile karar kümesine sıfırlayarak closure'ları opak sınırlar olarak ele alması için derleyiciyi zorlayın.
Artılar: Genel uzmanlaşma şişkinliğini sınırlayarak bazı optimizasyon aşırı yüklerini azaltabilir.
Eksiler: Tamamen inleme ve kaçış analizini engeller, her sınırda yığın tahsisini zorlar ve performansı önemli ölçüde kötüleştirir.
Çözüm C: Derleyicinin kaçışmayan bağlamları tanıması için küçük yardımcı fonksiyonlar üzerinde @inline(__always) kullanarak ve protokol yöntemleri üzerinde @escaping açıklamalarını kaçınarak closure zincirlerini yeniden yapılandırın.
Artılar: Fonksiyonel sözdizimini korurken, SIL seviyesindeki kaçış analizinin yığın güvenliğini kanıtlamasına izin verir; iç döngülerin vektörleştirilmesini mümkün kılar.
Eksiler: Protokol varoluşları veya dolaylı enum durumları aracılığıyla yanlışlıkla kaçışı önlemek için dikkatli bir kod yapısı gerektirir.
Seçilen Çözüm: DSP zincirini, protokol tabanlı varoluşlar yerine somut genel işlevler kullanarak yeniden yapılandırarak Çözüm C'yi uyguladık ve closure'ların kaçışmayan kalmasını sağladık. Optimasyonu SIL incelemesi ile doğruladık (swiftc -emit-sil).
Sonuç: Yığın tahsisleri ses buffer başına 16'dan sıfıra düştü, işleme gecikmesini 12ms'den 0.8ms'ye düşürdü, kesintileri ortadan kaldırdı ve işlevsel API tasarımını korudu.
Neden bir closure, seçenekli bir özelliğe atandığında yığın tahsisi otomatik olarak zorlanır, o özellik işlev döndükten sonra asla erişilmasa bile?
Bir closure, yığın çerçevesinden daha uzun süreli bir saklamaya atandığında—Optional özellikleri de dahil—derleyici, kaçarak işlemeyi varsayılan olarak üstlenmek zorundadır. Swift'in sahiplik modeli, saklanan her referans türünün (closure bağlamları dahil) ARC takibi için kararlı bir bellek konumu korumasını gerektirir. Yığın belleği değişkendir ve işlev çıkışında geri alınır, bu nedenle derleyici closure bağlamını gelecekteki bir erişim olasılığını karşılamak için yığından heap'e yükseltir. Bu, closure'ın kendisi için (işlev işaretçisi ve bağlam işaretçisi) kalıcı alan gerektirdiğinden weak veya unowned opsiyonel özelliklerde bile gerçekleşir.
Swift, bir closure'ın @escaping tür parametre kısıtlaması olan bir genel işlevle geçildiğinde kaçış analizini nasıl yönetir?
Swift'teki genel işlevler, dayanıklılığı korumak için çağrılacak yerlerinden bağımsız olarak derlenir. Eğer genel bir parametre T, @escaping ile kısıtlanırsa, derleyici en kötü durumu ele alan bir kod yayması gerekir: closure'ın bilinmeyen bir bağlama kaçması. Bu nedenle, derleyici, çağrılacak yerlerde özel görünümler sağlasa bile, @escaping kısıtlaması olan genel işlevlere geçirilen closurelar için yığın tahsis optimizasyonlarını devre dışı bırakır. Closure sınırda kutulanır ve yığına yükseltilir, bu da genel ABI'yi karşılamak için yapılırken, uzmanlaşmış optimizasyonların dayanıklılık sınırları veya modül sınırları boyunca yayılmasını engeller.
Yığın tahsisli ve yığın tahsisli olmayan closure bağlamları arasında ayrım yapan belirli SIL talimatları nelerdir ve bu durum tahliye yollarını nasıl etkiler?
SIL'de, alloc_stack, closure bağlamını yığında tahsis eder ve dealloc_stack ile kapsam dışına çıktığında temizlenir. Aksine, alloc_box bir yığın tahsisli referans sayımlı kutu oluşturur ve strong_release ile eşleştirilir. Temel fark, temizleme yolundadır: alloc_stack bağlamları, ARC trafiği olmadan yığın işaretçisi hareketi ile temizlenirken, alloc_box bağlamları ARC azaltmaları ve potansiyel tahliyeler gerektirebilir. Adaylar genellikle partial_apply talimatlarının, yığın tahsis alanına göre değerleri farklı biçimlerde yakaladığını, yığın depolamaya değerle yakalama ve yığın kutularında referansla yakalama olarak buna göre değiştiğini gözden kaçırırlar ve bu modların karıştırılmasının (örneğin, bir kaçışmayan closure'de değişken bir referans türü yakalamak) yine de referansın kendisi için yığın tahsisini gerektirdiğini unuturlar.