Sobes.tech
Middle+

Opowiedz o mechanizmie optymalizacji kopiowania w iOS.

sobes.tech AI

Odpowiedź od AI

Mechanizm optymalizacji kopiowania w iOS i macOS, często związany z używaniem struktury Copy-on-Write (CoW) lub semantyką wartości, pozwala unikać zbędnego kopiowania danych podczas przekazywania struktur lub klas z semantyką wartości, takich jak tablice, słowniki, łańcuchy znaków, a nawet własne struktury, jeśli implementują protokół Equatable i Hashable (niekoniecznie, ale w typowych przypadkach).

Istota CoW:

  1. Tworząc kopię struktury (np. przypisując jedną zmienną do drugiej, przekazując do funkcji przez wartość), nie dochodzi do rzeczywistego kopiowania danych. Obie zmienne wskazują na te same dane w pamięci. Licznik odwołań do danych rośnie.
  2. Kopiowanie jest odraczane do momentu, gdy jedna z kopii spróbuje zmienić dane.
  3. Dopiero w momencie zmiany dane są faktycznie kopiowane, a zmiana jest dokonywana na nowej kopii. Pierwotna kopia nadal wskazuje na dane początkowe (jeśli ma jeszcze co najmniej jednego właściciela).

Zalety:

  • Zmniejszenie kosztów operacji kopiowania, szczególnie dla dużych ilości danych (tablic, łańcuchów znaków).
  • Zmniejszenie użycia pamięci, ponieważ wiele bytów może współdzielić te same dane.
  • Wzrost wydajności przy przekazywaniu struktur z semantyką wartości.

Przykłady, gdzie CoW jest aktywnie wykorzystywane:

  • NSArray, NSDictionary, NSString: W Objective-C te klasy używają CoW do optymalizacji.
  • Array, Dictionary, String w Swift: Te podstawowe typy w Swift mają semantykę wartości i korzystają z CoW.
var array1 = [1, 2, 3]
var array2 = array1 // Brak kopiowania danych, obie zmienne wskazują na te same dane

array2.append(4) // Dochodzi do rzeczywistego kopiowania danych dla array2, następnie append
// Teraz array1 = [1, 2, 3], a array2 = [1, 2, 3, 4]

Wady:

  • Małe dodatkowe koszty związane z sprawdzaniem konieczności kopiowania przy każdej zmianie.
  • Możliwość ukrytych kopii, które mogą wpłynąć na wydajność w nieoczekiwanych miejscach.

Ważne jest, aby pamiętać, że CoW działa efektywnie dla struktur z semantyką wartości (struct, enum). Klasy mają semantykę odwołań, i podczas przypisania lub przekazania instancji klasy, kopiowana jest tylko referencja, a nie dane obiektu.

W Swift większość wbudowanych kolekcji i łańcuchów znaków działa z semantyką wartości i korzysta z CoW pod maską. Tworząc własne struktury zawierające typy odwołaniowe (np. klasy), CoW będzie stosowane do samej struktury, ale nie do zawartości typu odwołaniowego. Zmiana właściwości będącej typem odwołaniowym wewnątrz struktury nie wywoła CoW dla całej struktury, ale może wymagać CoW dla samego typu odwołaniowego, jeśli go obsługuje.

Aby ręcznie zaimplementować CoW w własnych typach danych w Swift, można użyć kombinacji typu odwołaniowego do przechowywania danych (np. klasy z wewnętrzną strukturą) i sprawdzania unikalności odwołania przed zmianą (np. isKnownUniquelyReferenced).

class Container<T> {
    var value: T

    init(_ value: T) {
        self.value = value
    }
}

struct CustomStruct {
    private var _container: Container<[Int]>

    var data: [Int] {
        get { return _container.value }
        set {
            // Sprawdzamy, czy kontener jest unikalnie referencjonowany
            // Jeśli nie, tworzymy nową kopię kontenera i danych
            if !isKnownUniquelyReferenced(&_container) {
                _container = Container(newValue)
            } else {
                // Jeśli tak, modyfikujemy dane bezpośrednio w bieżącym kontenerze
                _container.value = newValue
            }
        }
    }

    init(data: [Int]) {
        _container = Container(data)
    }
}

var s1 = CustomStruct(data: [1, 2, 3])
var s2 = s1 // CoW: _container w s1 i s2 wskazują na ten sam egzemplarz Container

s2.data.append(4) // Wywołuje setter data.
                   // isKnownUniquelyReferenced dla _container w s2 będzie false (bo s1 też na niego wskazuje).
                   // Tworzy się nowy Container z kopią [1, 2, 3, 4], s2._container wskazuje na niego.
                   // s1._container nadal wskazuje na stary Container z [1, 2, 3].

print(s1.data) // Wypisze: [1, 2, 3]
print(s2.data) // Wypisze: [1, 2, 3, 4]