Bu tasarım kararı, Swift'in standart kütüphane koleksiyonları için değer semantiğine temel bağlılığından kaynaklanmaktadır. Objective-C'nin NSMutableDictionary veya C++'nin std::unordered_map'ının referans semantiğini sergilemesi veya iç düğümlere dışsal işaretçiler sağlaması yerine, Swift Dictionary ve Set'i saf değer türleri olarak ele alır. Swift bu koleksiyonlar için Copy-on-Write (COW) optimizasyonları benimsediğinde, referans türü performansını korurken değer türü güvenliğini sağlama konusunda mühendislere kritik bir karar vermeleri gerekti. Değişiklik durumunda indekslerin geçersiz kılınması kararı, hash tablosu büyümesi, çarpışma çözümü veya giriş silme sırasında yeniden tahsis edilen depolama alanına işaret eden erişimlerin önüne geçmek için resmileştirildi.
Ana problem, COW semantikleri ile hash tablosu uygulama detayları arasındaki etkileşimden kaynaklanmaktadır. Bir Dictionary ekleme veya silme yoluyla değiştiğinde, yük faktörünün belirli eşiklerin üzerine çıkması durumunda bir yeniden boyutlandırma tetikleyebilir ve bu da yeni, daha büyük bir arabellek ayırarak tüm girişleri yeniden hash etmesine neden olur. Değişiklik öncesinde oluşturulan herhangi bir Index değeri, eski arabelleğin fiziksel belleğine bir offset veya işaretçi kapsar. O indeks değişiklik sonrası erişilirse, serbest bırakılmış belleği referans alır (use-after-free) veya yanlış kutulardan veri döndürür. Swift, bağımsız Dictionary kopyaları arasında her Index değerinin ömrünü takip edemediğinden (değer semantikleri kopyalamaya sınırsız izin verir), tüm açık indeksleri güvenli bir şekilde güncelleyemez. Bu nedenle, dil, bellek güvenliği garantilerini korumak için bu tür indeksleri geçersiz olarak ilan etmek zorundadır.
Swift, Dictionary'nin iç depolama başlığına bir nesil sayısı veya versiyon numarası gömerek bu problemi çözer. Her Index, oluşturulma anında bu nesil tanımlayıcısını alır. Dictionary değiştiğinde, çalışma zamanı bu nesil sayısını artırır ve potansiyel olarak temel arabelleği yeniden tahsis eder. Eski bir Index'in sonraki bir kullanımı, saklanan nesil numarasını mevcut olanla karşılaştırır; bir uyuşmazlık, deterministik bir çalışma zamanı hatasını (ön koşul hatası) tetikler. Bu yaklaşım, bellek güvenliği ve değer semantiği bütünlüğü lehine değişiklikler boyunca indeks istikrarını feda eder. COW optimizasyonu için çalışma zamanı, değişiklikten önce referans sayımlarını kontrol eder: eğer benzersiz referansa sahip ise, yerinde değişiklik yapar (indeksleri geçersiz kılar); eğer paylaşımlıysa, önce arabelleği kopyalar, böylece orijinal örneğin indeksleri geçerli kalır ve yeni kopyaya taze bir nesil sayısı verilir.
var marketData: [String: Double] = ["AAPL": 150.0, "GOOGL": 2800.0] let indexBeforeUpdate = marketData.index(forKey: "AAPL")! // Nesil 0 marketData["TSLA"] = 700.0 // Değişiklik nesli artırır, yeniden tahsis edebilir // Çalışma zamanı hatası: nesil 0'dan geçersiz indeks kullanarak erişilmeye çalışılıyor // let price = marketData[indexBeforeUpdate]
Bir geliştirme ekibi, canlı fiyat verilerini String ticker sembolleri anahtarlarıyla önbelleğe almak için Swift kullanarak iPad üzerinde yüksek frekanslı bir ticaret panosu inşa ediyordu. Hızlı güncellemeler sırasında UI render performansını optimize etmek için, tablo görünümü hücre konfigürasyonu sırasında tekrar eden hash hesaplamalarını önlemek için doğrudan Dictionary indekslerini görünüm modellerinde sakladılar. Ancak, arka planda çalışan WebSocket iş parçacıkları sözlüğe yeni fiyat noktaları eklediğinde, uygulama EXC_BAD_ACCESS hatasıyla rastgele çöküşler gösteriyor veya yeniden tahsis edilen bellek bölgelerinden bozulmuş veriler gösteriyordu, çünkü önbelleklenen indeksler hash tablosu kutularını referans alıyordu.
İlk çözüm, referans semantikleri ve değer semantikleri arasındaki karışıklığı önlemek için, uygulama yaşam döngüsü boyunca süreklilik sağlamak adına NSMutableDictionary'ye taşınmaktı. Ancak bu, görünüm modelleri arasında değer türü izolasyonunu bozarak, arka planda bulunan kuyruklar ve ana iplik arasında sözlükleri kopyalarken istenmeyen veri paylaşımı ve yarış koşulları yarattı. Ayrıca, NSMutableDictionary Swift'in genel tür güvenliğinden yoksundu ve yapı gibi değer türleri için pahalı köprüleme işlemleri gerektiriyordu, performansı düşüren kutulama işlemlerine yol açıyordu.
İkinci çözüm olarak, UnsafeMutablePointer kullanarak kararlı düğüm bellek adreslerini manuel olarak yönetmek için özel bir açık adresleme hash tablosu uygulamayı keşfettiler. Ancak bu, Swift'in indeks geçersiz kılma mekanizmasını tamamen atlamak anlamına geliyordu. Bu, saklanan indeksler için deterministik işaretçi istikrarı sağlardı ve arama sırasında yeniden hash etme aşamasında O(1) erişim imkanı sunardı. Ancak bu yaklaşım, malloc ve free ile manuel bellek yönetimi gerektiriyordu, bu nedenle düğümler doğru bir şekilde silinmezse bellek sızıntısı riski taşıyordu. Ayrıca, bu, Swift'in COW optimizasyonlarını çiğneyerek, sözlüğün her bir kopyasının heap' üzerinde tahsis edilmiş arabelleğin tam derin kopyasını talep etmesi anlamına geliyordu, on binin üzerindeki veri setleri için performansı mahvediyordu.
Sonuç olarak, ekip üçüncü çözümü seçti: indeks önbelleğini tamamen ortadan kaldırarak, bunun yerine görünüm modellerinde anahtar dizileri (String ticker'ları) saklayarak, her hücre konfigürasyon döngüsünde anahtar-tabanlı aramalar yaptı. Bu yaklaşım, Swift'in değer semantiğini ve bellek güvenliği garantilerini korurken yine de O(1) ortalama koşulda arama performansı sunuyordu. Her erişimde anahtarın yeniden hash edilmesi maliyeti olsa da, modern Swift dize hashing'i oldukça optimize edilmişti ve güvenlik garantileri göz önüne alındığında, önemsiz mikro saniye düzeyindeki performans cezası kabul edilebilir bulundu. Ayrıca, kararsız indekslere dayanmayacak deterministik sıralama sağlamak için açık kaynaklı Swift Collections paketinden OrderedDictionary türünü benimsediler.
Sonuç olarak, sonraki üç aylık izleme sürecinde EXC_BAD_ACCESS çöküşlerinin tamamen ortadan kaldırılması sağlandı. Uygulamanın bellek ayak izi 50,000 eşzamanlı fiyat girişiyle bile kararlı kaldı ve kod tabanı, UnsafeMutablePointer işlemlerinin karmaşıklığı olmadan önemli ölçüde daha sürdürülebilir hale geldi. Ekip, değişiklik sınırları boyunca Dictionary veya Set indekslerinin saklanmasını yasaklayan katı bir mimari kılavuz oluşturdu ve bu kalıbı gelecekteki regresyonları önlemek için iç wiki'lerinde belgeler halinde kaydetti.
Neden Swift'in Array'i bazı değişikliklerden sonra indeks yeniden kullanımına izin verirken, Dictionary buna izin vermez, her ikisi de değer türleri ve COW semantikleri ile mi?
Array indeksleri temel adresin sürekli depolama alanından offsetleri temsil eden hafif Int değerleridir. Arabelleğe alma kapasitesinin ötesinde bir şey eklemek gibi yeniden boyutlandırmayı tetikleyen Array değişiklikleri, arabelleği taşıyarak teknik olarak indeksleri geçersiz kılar; ancak Array indeksleri doğrulama için nesil meta verisi taşımaz, bu nedenle önbelleğe almak için tehlikeli hale gelir ama açıkça kontrol edilmezler. Dictionary indeksleri ise, seyrek bir hash tablosunda kutu içi offsetleri dahil karmaşık bir iç durumu kapsar. Hash tablosu girişleri yeniden hash etme (yük faktörü eşikleri veya çarpışma çözümü ile tetiklenir) sırasında öngörülemez bir şekilde hareket ettiğinden, tam sayı offsetlerinin anlamsal anlamı kaybolur. Swift, teorik olarak Dictionary için mantıksal indeks dolaylılığı uygulayabilir, ancak bu her erişimi yavaşlatacak ekstra bir işaretçi takip etmeyi gerektirir. Bu nedenle, Dictionary ve Set nesil sayıları aracılığıyla indeksleri agresif bir şekilde doğrulayıp geçersiz kılarken, Array indeksleri geçerliliği sağlamaktan programcıya bırakmaktadır ve bu, sürekli ve hashlenmiş depolama arasındaki farklı performans ve güvenlik ticaretlerini yansıtır.
Copy-on-Write mekanizması, bir Dictionary değişikliğinin mevcut örnekte indeks geçersiz kılmayı gerektirip gerektirmediğini veya taze indekslerle yeni bir kopya oluşturulmasını nasıl belirler?
Swift, dahili arabellekteki referans sayımlarını kullanır (_NativeDictionary). Herhangi bir değişiklik öncesinde, çalışma zamanı isUniquelyReferencedNonObjC fonksiyonunu çağırarak arabelleğin referans sayısını kontrol eder. Eğer sayım bir (benzersiz sahiplik) ise, değişiklik yerinde gerçekleşir, yalnızca bu belirli örnekteki indeksleri geçersiz kılarak nesil sayısını artırır. Eğer referans sayısı birden fazla (paylaşımlı sahiplik) ise, Swift yeni bir arabellek ayırır, tüm öğeleri kopyalar ve değişikliği yeni kopyada gerçekleştirir. Orijinal örnek değişmez, geçerli indekslere sahipken, yeni kopya taze bir nesil sayısıyla başlar (etkili olarak indeks sıfır). Bu ayrım, değer semantiği açısından kritik öneme sahiptir: bir değer atamasından sonra her iki değişken de depolamayı paylaşır, biri değiştiğinde, tembel kopyalama tetiklenir. Değişiklik noktası, mantıksal ayrımın gerçekleştiği yerdir ve bu, değişen örneğin değişiklik öncesinde benzersiz sahipliğe sahip olmasını sağlar.
Swift'in Dictionary indeks geçersiz kılınması, raw depolamaya erişmek için withUnsafeMutablePointer veya Unmanaged kullanılarak aşılabilir mi ve bu, ne tür felaket riskleri doğurur?
Teknik olarak, UnsafeMutablePointer ve Unmanaged, Dictionary'nin dahili depolamasına withUnsafeMutablePointer aracılığıyla doğrudan erişim sağlayabilir veya Dictionary'yi ham byte'lara dönüştürerek bu erişimi elde edebilir. Ancak bu, tanımsız bir davranış oluşturur. Dictionary'nin dahili yapısı, belli bir Swift versiyonu arasında değişime tabi ve opak olduğundan, doğrudan işaretçi manipülasyonu nesil sayısı kontrollerini atlatır ve yeniden tahsis sırasında serbest bırakılmış belleğe erişim sağlar. Ayrıca, hash tabloları, silinmiş girişler için dolgu bitmap'leri ve mezar işaretçileri hakkında karmaşık invariatlar korur. Manuel işaretçi manipülasyonu bu invariatları bozabilir ve tarama dizileri sırasında sonsuz döngülere, sessiz veri bozulmasına veya sonraki Dictionary işlemlerinde çöküşlere neden olabilir. Swift'in güvenlik modeli bunu açıkça yasaklar; dengeli referansları korumanın tek güvenli mekanizması, anahtarları kullanmak (her erişimde yeniden hash edilir) veya koleksiyondan ayrı bir diziye değer kopyalamaktır.