Swift stosuje strategię optymalizacji nazwaną Copy-on-Write (COW) dla typów wartości, które zawierają pamięć przydzieloną na stercie. Zamiast natychmiast wykonywać głęboką kopię w momencie przypisania, język opóźnia duplikację do momentu, gdy instancja jest faktycznie modyfikowana. Osiąga to poprzez wewnętrzne odwołanie typu wartości do wspólnej instancji klasy, jednocześnie używając funkcji uruchomieniowej isKnownUniquelyReferenced do wykrywania, kiedy licznik referencji jest równy jeden. Gdy zachodzi modyfikacja i referencja jest unikalna, bufor jest modyfikowany na miejscu; w przeciwnym razie tworzona jest kopia przed zapisaniem, co pozwala zachować semantykę wartości bez kary wydajnościowej związanej z pochopnym kopiowaniem.
Nasz zespół budował wydajny pipeline do przetwarzania obrazów, w którym zdefiniowaliśmy niestandardową strukt Image otaczającą dużą pamięć CVPixelBuffer. Problem pojawił się podczas profiliowania: zastosowanie każdego filtra tworzyło trzy pośrednie kopie obrazów 4K, co prowadziło do przydziałów 300MB na klatkę i wywoływało ostrzeżenia pamięci na urządzeniach iPad.
Rozważaliśmy trzy różne podejścia do rozwiązania tego wąskiego gardła. Pierwsze podejście polegało na przekształceniu Image z strukt na klasę. To całkowicie wyeliminowało kopie dzięki używaniu semantyki odniesienia, ale wprowadziło poważne błędy związane z bezpieczeństwem wątków, gdy wiele łańcuchów przetwarzania przypadkowo dzieliło i modyfikowało te same dane pikseli jednocześnie, prowadząc do artefaktów wizualnych i warunków wyścigu, które były trudne do debugowania.
Drugie podejście zachowało oznaczenie strukt, ale wprowadziło ręczne głębokie kopiowanie za pomocą UnsafeMutablePointer i optymalizacji memcpy. Zapewniało to bezpieczeństwo poprzez surową semantykę wartości, ale profilowanie pokazało, że pochłaniało 800% więcej czasu CPU niż nasz cel, ponieważ każdy argument funkcji wywoływał przydział pamięci 12MB i operację kopiowania bitowego.
Trzecie podejście wdrożyło ręcznie semantykę Copy-on-Write. Stworzyliśmy prywatną klasę ImageBuffer, aby przechowywać rzeczywisty CVPixelBuffer, sprawiliśmy, że strukt Image przechowuje odniesienie do tej klasy, i wdrożyliśmy wszystkie metody mutujące, aby sprawdzać isKnownUniquelyReferenced przed modyfikacją:
final class ImageBuffer { var pixels: CVPixelBuffer init(_ buffer: CVPixelBuffer) { self.pixels = buffer } } struct Image { private var buffer: ImageBuffer mutating func applyFilter(_ filter: Filter) { if !isKnownUniquelyReferenced(&buffer) { buffer = ImageBuffer(buffer.pixels.deepCopy()) } filter.process(buffer.pixels) } }
Jeśli referencja nie była unikalna, najpierw zduplikowaliśmy bufor. Wybraliśmy to rozwiązanie, ponieważ zapewniało bezpieczeństwo semantyki wartości Swift przy jednoczesnym eliminowaniu zbędnych kopii podczas operacji tylko do odczytu.
Wynik zmniejszył ciśnienie pamięci o 94% i poprawił czas przetwarzania klatki z 120ms do 18ms na obraz, co pozwoliło aplikacji przetwarzać strumienie wideo w czasie rzeczywistym bez ograniczeń cieplnych na starszym sprzęcie.
Dlaczego nie możemy ręcznie sprawdzić liczników referencji zamiast używać isKnownUniquelyReferenced?
Wielu kandydatów sugeruje śledzenie liczników referencji ręcznie lub porównywanie adresów pamięci. Jednak isKnownUniquelyReferenced to nie tylko sprawdzenie licznika; zawiera wbudowane w kompilator bariery, które zapobiegają optymalizacji reorganizacji operacji pamięci. Bez tej intrynzycznej funkcji kompilator może optymalizować sprawdzenie unikalności, lub uruchomienie może zwracać fałszywe pozytywy z powodu interakcji z uruchomienie Objective-C lub konwersji mostowych, które utrzymują dodatkowe nieposiadające referencje niewidoczne dla standardowego liczenia ARC.
Jak COW współdziała z egzekwowaniem ekskluzywności w Swift?
Kandydaci często wierzą, że COW działa automatycznie dla wszystkich typów wartości zawierających klasy. Przegapiają to, że zasady ekskluzywności Swift wymagają, aby modyfikacje miały wyłączny dostęp. Podczas wdrażania niestandardowego COW sprawdzenie isKnownUniquelyReferenced musi mieć miejsce przed rozpoczęciem modyfikacji, a wymiana bufora musi odbywać się atomowo w odniesieniu do sprawdzenia. Naruszenie tego przez utrzymywanie wielu referencji podczas sprawdzenia może wywołać naruszenia ekskluzywności w czasie wykonania lub spowodować fałszywe negatywy w wykrywaniu unikalności.
Kiedy COW nie zapobiega kopiowaniu w kontekście równoległym?
Z modelem współbieżności Swift 5.5 kandydaci zakładają, że COW zapewnia bezpieczną mutację wątków. Jednak COW zapewnia bezpieczeństwo tylko w ramach jednego wątku. Podczas przekazywania wartości przez granice aktora lub oznaczania ich jako Sendable, kompilator może wymuszać pochopne kopiowanie w celu zachowania izolacji. Dodatkowo, jeśli klasa zaplecza zawiera obiekty Objective-C, isKnownUniquelyReferenced może ostrożnie zwracać fałsz z powodu implementacji słabych referencji w Objective-C, co prowadzi do niepotrzebnych kopii, które wymagają przebudowy modelu własności w celu optymalizacji.