Swift'in bellek sahipliğine yönelik evrimi, ARC'nin (Otomatik Referans Sayımı) tanıtılmasıyla başladı; bu, bellek yönetimini otomatik olarak sağlamak için derleme zamanında retain, release ve copy işlemleri ekler. ARC, bellek güvenliğini sağlarken, yüksek performanslı alanlar, gerçek zamanlı sistemler veya yüksek frekanslı veri işleme gibi alanlarda engelleyici olabilecek çalışma zamanı yükü getirir. Bu sorunu aşmak için, Swift 5.9, değer yaşam döngüleri ve değişkenlik hakkında açık sözleşmeler sunan parametre sahipliği değişkenlerini - özellikle borrowing, consuming ve mevcut inout'u tanıttı.
Temel sorun, Swift'in varsayılan kopyalama anlamsallığıyla ilgilidir: bir sınıf örneği veya yığın üzerinde ayrılmış bellek içeren bir değer türü (örneğin Array veya String) geçirildiğinde, derleyici genellikle aramayı gerçekleştirenin çağrı süresi boyunca güçlü bir referansa sahip olmasını sağlamak için bir retain çağrısı üretir. Değer türleri için, bu, referans sayısı birden büyükse COW'yi (Kopyala-Üzerine Yaz) tetikleyebilir. Bu örtülü kopyalama, güvenliği sağlarken, belirli bir gecikme gerektiren sıkı döngüler veya eşzamanlı ortamlarda öngörülebilir performans düşüşleri yaratır.
Çözüm, mülkiyet aktarım anlamsallığından yararlanır: borrowing parametresi, aramayı gerçekleştirenin sahiplik talep etmeden geçici, değiştirilemez bir referans aldığını gösterir ve bu, derleyicinin retain/release çiftlerini tamamen atlamasına olanak tanır. consuming parametresi, çağıranın, aramayı gerçekleştirenin değerinin yok edilmesinden ya da daha fazla aktarımından sorumlu olduğu şekilde sahipliği aktardığını belirtir; bu da işlemi bir hareket olarak değerlendirerek retain çağrılarını önler. Değer türleri için, consuming, temel bellek kopyalamadan bit düzeyinde hareketleri mümkün kılarken, borrowing da okuma-yazma erişimlerini garanti ederek COW'yi tetiklemez.
import Foundation final class AudioBuffer { var data: [Float] init(size: Int) { data = Array(repeating: 0.0, count: size) } } // Varsayılan: Girişte retain, çıkışta release func processDefault(_ buffer: AudioBuffer) -> Float { return buffer.data.reduce(0, +) } // Borrowing: ARC trafiği yok, değiştirilemez referans func processBorrowing(_ buffer: borrowing AudioBuffer) -> Float { return buffer.data.reduce(0, +) } // Consuming: Mülkiyet aktarımı, retain yok, arayan yaşam döngüsünü yönetiyor func processConsuming(_ buffer: consuming AudioBuffer) -> [Float] { return buffer.data // İç verilerin veya tam tamponun mülkiyetini aktar } // Hareket semantiğini gösteren kullanım var buffer = AudioBuffer(size: 1024) let sum = processBorrowing(buffer) // Retain yok processConsuming(buffer) // Taşı, burada buffer artık geçerli değil
Ekibimiz, iOS için gerçek zamanlı bir ses sentez motoru geliştirdi; burada ses render geri çağrısı, özel bir yüksek öncelikli iş parçacığında çalışıyor. Sistem, karmaşık filtre zincirleri sırasında ara sıra ses kesilmeleri (bozukluklar) yaşamaya başladı ve profil oluşturma, bunun, örnek tamponları işleme düğümleri arasında geçirmenin neden olduğu ARC retain/release trafiğinden kaynaklandığını ortaya çıkardı. Bu yük, geri çağrının sesli izleri önlemek için 3 milisaniyede tamamlanması gereken sıkı gerçek zamanlı kısıtlamayı ihlal etti.
İlk düşünülen çözüm, tüm ses tamponlarını UnsafeMutablePointer<Float>'a dönüştürerek bellek yönetimini manuel olarak yapmaktı. Bu yaklaşım, tamponları ham C işaretçileri olarak ele alarak ARC'yi tamamen ortadan kaldırırdı. Ancak sıfır yükün avantajları, bellek güvenliğini ihlal eden, kullanılmadıktan sonra serbest bırakılan hatalara yatkın ve karışık deneyim seviyelerine sahip bir ekibin bakımını zorlaştırdığı önemli dezavantajlarla karşılaştı.
İkinci çözüm, referans sayısını manuel olarak kontrol etmek için Unmanaged<T> kullanmayı içeriyordu; sınıf örneklerini sarmalayarak belirli sınır noktalarında takeRetainedValue() ve passRetained() kullanılıyordu. Bu biraz tür güvenliği sağlasa da, dezavantajları aşırı ayrıntılı olmasına ve referans sayısı dengesizliklerinin sızıntılara veya çöküşlere neden olma riskine dahil oluyordu. Ayrıca, her kod yolunu dikkatlice denetlemeyi gerektiriyordu, bu da kod tabanını yeniden yapılandırmaya karşı kırılgan hale getiriyordu.
Üçüncü çözüm, Swift 5.9'un sahiplik değişkenlerini benimsemekti; ses hattını, okuma için borrowing AudioBuffer ve asenkron aşamalar arasında tampon sahipliğini aktarmak için consuming AudioBuffer kullanarak yeniden yapılandırdı. Avantajları içinde sıfır maliyetli soyutlama ile tam güvenlik sağlama vardı: borrowing, filtre okumaları için retain çağrılarını ortadan kaldırdı, consuming ise büyük ses verilerini kopyalamadan pipelining aşamaları arasında hareket semantiklerini mümkün kıldı. Tek dezavantaj, Xcode 15'e güncelleme yapma ve sahiplik kısıtlamalarını kolay ifade edemeyen bazı protokol odaklı arayüzleri yeniden tasarlama gerekti.
Üçüncü çözümü, bellek güvenliğinden ödün vermeden gereken performans özelliklerini sağladığı için tercih ettik. borrowing'i ses geri çağrısının sıcak yolunda uygulayarak, gerçek zamanlı iş parçacığında ARC trafiğini sıfıra indirdik ve Swift'in tür güvenliği garantilerini koruduk. consuming deseni, üreticiden tüketici iş parçacığına mülkiyeti açıkça aktararak kendi ring tamponu uygulamamızı basitleştirdi, pahalı kopyalama işlemlerinden kaçındık.
Sonuç olarak, ses kesilmelerinin tamamen ortadan kaldırılmasını ve ses iş parçacığının ortalama CPU kullanımını zirve işlem yükleri sırasında %45'ten %28'e düşürmeyi başardık. Kod tabanı tamamen bellek güvenli kaldı ve yeniden yapılandırma sırasında, UnsafeMutablePointer yaklaşımında çöküşlere yol açabilecek birçok olası yaşam süresi hatasını derleme zamanında hata yakaladık. Ayrıca, açık sahiplik notları API sözleşmesi için belgeler olarak hizmet etti ve gelecekteki geliştiriciler için kodun bakımını kolaylaştırdı.
Neden borrowing'in bir değer türü parametresine uygulanması, temel depolama paylaşıldığında Kopyala-Üzerine Yaz (COW) tetikleyicilerini önler ve bu inout'dan nasıl farklıdır?
COW (kopyala-üzerine yaz) kullanan bir değer türü (örneğin Array veya Dictionary), borrowing aracılığıyla geçirildiğinde, derleyici aramayı gerçekleştirenin o bağlama göre değeri değiştiremeyeceğini garanti eder. Değiştirme imkânı olmadığından, Swift bilgiyi referansla geçirebilir, referans sayısını kontrol etmeden veya belleği kopyalamadan, diğer referanslar var olsa bile. Bununla birlikte, inout değişikliği mümkün kılar; bu, derleyicinin yazmadan önce referans sayısını bir olduğundan emin olmasını zorlar; eğer olmazsa, diğer referanslar için değer anlamsallığını korumak amacıyla pahalı bir kopyayı tetikler.
Derleyici, consuming parametre geçişini hangi belirli koşullar altında reddedecek ve consume operatörü bu durumu nasıl çözüme kavuşturur?
Derleyici, bir argümanın consuming parametresine geçişini, argümanın bu değerin son kullanımı olmadığı (yani, sonraki erişimlerin Tekillik Yasası'nı ihlal edeceği) durumlarda reddeder. Kopyalanamaz türler için, bu kesin bir hata çünkü değeri hem tüketim hem de sonraki kullanı karşılayacak şekilde çoğaltmak mümkün değildir. consume operatörü, bir değerin yaşam döngüsünün belirli bir noktada sona erdiğini belirterek, derleyiciye o konumu son kullanım olarak değerlendirmesi talimatını verir; bu sayede hareket işlemi devam edebilirken orijinal bağlamayı sonraki kod için geçersiz kılar.
Parametre sahiplik değişkenleri, protokol şahitlik tablolarıyla nasıl etkileşir, genel işlevler ile varoluşsal türler kullanıldığında ve protokol gereksinimlerinde neden kısıtlar getirir?
Borrowing ve consuming gibi sahiplik değişkenleri, protokol gereksinimleri (örneğin, func process<T: AudioProtocol>(_ buffer: borrowing T)) içinde, derleyici tarafından sahiplik sözleşmesine saygı gösteren özel kod üretilen genel işlevlerde tam olarak desteklenmektedir. Ancak protokol gereksinimleri, yöntemlerinde sahiplik değişkenlerini tanımlayamaz; protocol P { func method(_ x: consuming Self) } yazamazsınız çünkü varoluşsal konteynerler (any P) dinamik dağıtım kullanmakta ve şu an için borrowing ve consuming anlamsallığını ayırt edecek meta veriye sahip değildir. Bu durum, geliştiricilerin taşınmaz türlerle çalışırken veya sahiplik aracılığıyla ARC davranışını optimize ederken genel koşulları (<T: P>) kullanmaya yönlendirir.