Zalety semantyki kopiowania przy zapisie

10

Zastanawiam się, jakie są możliwe zalety kopiowania przy zapisie? Oczywiście nie oczekuję osobistych opinii, ale praktyczne scenariusze z rzeczywistego świata, w których może to być technicznie i praktycznie korzystne w namacalny sposób. A przez namacalny mam na myśli coś więcej niż oszczędzanie pisania &postaci.

Aby wyjaśnić, to pytanie jest w kontekście typów danych, w których konstrukcja przypisania lub kopii tworzy niejawną płytką kopię, ale modyfikacje w niej tworzą niejawną głęboką kopię i stosuje do niej zmiany zamiast oryginalnego obiektu.

Powodem, dla którego pytam, jest to, że nie znajduję żadnych zalet posiadania COW jako domyślnego domyślnego zachowania. Używam Qt, który ma COW zaimplementowany dla wielu typów danych, praktycznie wszystkich, które mają jakieś dynamiczne przydzielanie pamięci. Ale jak to naprawdę wpływa na użytkownika?

Przykład:

QString s("some text");
QString s1 = s; // now both s and s1 internally use the same resource

qDebug() << s1; // const operation, nothing changes
s1[o] = z; // s1 "detaches" from s, allocates new storage and modifies first character
           // s is still "some text"

Co wygrywamy, używając COW w tym przykładzie?

Jeśli wszystko, co zamierzamy zrobić, to użyć operacji const, s1jest redundantne, może równie dobrze użyć s.

Jeśli zamierzamy zmienić wartość, wówczas COW opóźnia kopię zasobu tylko do pierwszej operacji non-const, przy (choć minimalnym) koszcie zwiększenia liczby odwołań dla niejawnego udostępniania i odłączania od pamięci wspólnej. Wygląda na to, że cały koszt związany z COW jest bezcelowy.

Nie różni się zbytnio w kontekście przekazywania parametrów - jeśli nie zamierzasz modyfikować wartości, podaj jako const referen, jeśli chcesz zmodyfikować, albo wykonaj niejawną głęboką kopię, jeśli nie chcesz modyfikować oryginalny obiekt lub przekaż referencję, jeśli chcesz go zmodyfikować. Ponownie COW wydaje się niepotrzebnym narzutem, który niczego nie osiąga, i dodaje jedynie ograniczenie, że nie możesz modyfikować oryginalnej wartości, nawet jeśli chcesz, ponieważ każda zmiana odłączy się od oryginalnego obiektu.

Tak więc w zależności od tego, czy wiesz o COW, czy jesteś tego nieświadomy, może to skutkować kodem z niejasnym zamiarem i niepotrzebnym narzutem, lub całkowicie mylącym zachowaniem, które nie spełnia oczekiwań i powoduje, że drapiesz się po głowie.

Wydaje mi się, że istnieją bardziej wydajne i czytelniejsze rozwiązania, niezależnie od tego, czy chcesz uniknąć niepotrzebnej głębokiej kopii, czy zamierzasz ją wykonać. Więc gdzie jest praktyczna korzyść z COW? Zakładam, że musi to przynieść pewne korzyści, ponieważ jest wykorzystywany w tak popularnym i potężnym frameworku.

Co więcej, z tego, co przeczytałem, COW jest teraz wyraźnie zabronione w standardowej bibliotece C ++. Nie wiem, czy wady, które w nim widzę, mają coś z tym wspólnego, ale tak czy inaczej, musi być ku temu powód.

dtech
źródło

Odpowiedzi:

15

Kopiuj przy zapisie jest używany w sytuacjach, w których bardzo często tworzona jest kopia obiektu, a nie modyfikowana. W takich sytuacjach opłaca się.

Jak wspomniałeś, możesz przekazać stały obiekt, a w wielu przypadkach jest to wystarczające. Jednak const gwarantuje tylko, że osoba dzwoniąca nie może go zmutować (chyba że const_castoczywiście). Nie obsługuje przypadków wielowątkowości i nie obsługuje przypadków, w których istnieją wywołania zwrotne (które mogą mutować oryginalny obiekt). Przekazywanie obiektu COW według wartości stawia wyzwania związane z zarządzaniem tymi szczegółami przed deweloperem interfejsu API, a nie użytkownikiem interfejsu API.

Nowe zasady dla C + 11 zabraniają std::stringw szczególności COW . Iteratory ciągu muszą zostać unieważnione, jeśli bufor kopii zapasowej zostanie odłączony. Jeśli iterator był implementowany jako char*(W przeciwieństwie do string*indeksu i), iteratory nie są już ważne. Społeczność C ++ musiała zdecydować, jak często iteratory mogą być unieważniane, i zdecydowano, że operator[]nie powinien to być jeden z tych przypadków. operator[]na std::stringzwrotach a char&, które mogą być modyfikowane. Dlatego operator[]musiałby odłączyć ciąg, unieważniając iteratory. Uznano to za słabą wymianę handlową i w przeciwieństwie do funkcji takich jak end()i cend(), nie ma możliwości, aby poprosić o wersję const operator[]krótkiego od const rzucającego ciąg. ( powiązane ).

COW wciąż żyje i znajduje się poza STL. W szczególności uważam, że jest to bardzo przydatne w przypadkach, w których użytkownik moich interfejsów API nie ma uzasadnienia, aby oczekiwać, że za czymś, co wydaje się być bardzo lekkim przedmiotem, jest jakiś ciężki obiekt. Mogę chcieć użyć COW w tle, aby upewnić się, że nigdy nie będą musieli zajmować się takimi szczegółami wdrożenia.

Cort Ammon
źródło
Mutowanie tego samego ciągu w wielu wątkach wydaje się bardzo złym projektem, niezależnie od tego, czy używasz iteratorów, czy []operatora. Więc COW umożliwia zły projekt - nie brzmi to jak pożytek :) Punkt w ostatnim akapicie wydaje się słuszny, ale ja sam nie jestem wielkim fanem niejawnego zachowania - ludzie biorą to za pewnik, trudno jest ustalić, dlaczego kod nie działa zgodnie z oczekiwaniami, i zastanawiać się, dopóki nie wymyślą, co kryje się pod ukrytym zachowaniem.
dtech
Jeśli chodzi o punkt użycia, const_castwydaje się, że może on złamać COW równie łatwo, jak może przerwać przekazywanie przez stałą referencyjną. Na przykład QString::constData()zwraca a const QChar *- const_castto i COW zwija się - mutujesz dane oryginalnego obiektu.
dtech
Jeśli możesz zwrócić dane z COW, musisz to zrobić, zanim to zrobisz, lub zwróć dane w formie, która nadal jest świadoma COW ( char*oczywiście nie jest świadoma). Jeśli chodzi o ukryte zachowanie, myślę, że masz rację, są z tym problemy. Interfejs API zapewnia stałą równowagę między dwiema skrajnościami. Zbyt niejawne, a ludzie zaczynają polegać na specjalnym zachowaniu, tak jakby było to de facto częścią specyfikacji. Zbyt jawne, a interfejs API staje się zbyt nieporęczny, gdy ujawniasz zbyt wiele podstawowych szczegółów, które nie były tak naprawdę ważne i nagle zostały zapisane w specyfikacji interfejsu API.
Cort Ammon
Sądzę, że stringklasy uzyskały zachowanie COW, ponieważ projektanci kompilatora zauważyli, że duża część kodu kopiuje ciągi zamiast używać const-referencji. Gdyby dodali COW, mogliby zoptymalizować tę sprawę i uszczęśliwić więcej ludzi (i było to legalne, aż do C ++ 11). Doceniam ich pozycję: chociaż zawsze przekazuję ciągi przez stałe odniesienie, widziałem wszystkie te składniowe śmieci, które po prostu zmniejszają czytelność. Nienawidzę pisać const std::shared_ptr<const std::string>&tylko po to, by uchwycić prawidłową semantykę!
Cort Ammon
5

Wydaje się, że w przypadku ciągów znaków i pesymizacji bardziej powszechne przypadki użycia niż nie, ponieważ częstym przypadkiem ciągów są często małe ciągi, a tam narzut COW zwykle przewyższałby koszt zwykłego kopiowania małego ciągu. Mała optymalizacja bufora ma dla mnie znacznie większy sens, aby uniknąć przydziału sterty w takich przypadkach zamiast kopii ciągów.

Jeśli jednak masz cięższy obiekt, taki jak android, a chciałeś go skopiować i po prostu wymienić jego cybernetyczne ramię, COW wydaje się całkiem rozsądnym sposobem na zachowanie zmiennej składni przy jednoczesnym unikaniu konieczności głębokiego kopiowania całego Androida tylko w celu nadaj kopii wyjątkowe ramię. Uczynienie go po prostu niezmiennym jako trwała struktura danych w tym momencie może być lepsza, ale „częściowe COW” zastosowane na poszczególnych częściach Androida wydaje się uzasadnione w tych przypadkach.

W takim przypadku dwie kopie Androida dzieliłyby / wystąpiły ten sam tors, nogi, stopy, głowę, szyję, ramiona, miednicę itp. Jedyne dane, które byłyby między nimi różne i nie byłyby udostępnione, to ramię, które zostało wykonane unikalny dla drugiego Androida po nadpisaniu ramienia.


źródło
To wszystko jest dobre, ale nie wymaga COW i nadal podlega wielu szkodliwym skutkom. Ma to też pewną wadę - często możesz chcieć wykonać instancję obiektu, a nie mam na myśli instancji typu, ale skopiuj obiekt jako instancję, dlatego podczas modyfikacji obiektu źródłowego kopie są również aktualizowane. COW po prostu wyklucza tę możliwość, ponieważ każda zmiana w obiekcie „współdzielonym” go odłącza.
dtech,
Poprawność IMO nie powinna być „łatwa” do osiągnięcia, nie z zachowaniem niejawnym. Dobrym przykładem poprawności jest poprawność CONST, ponieważ jest wyraźna i nie pozostawia miejsca na dwuznaczności lub niewidoczne skutki uboczne. Posiadanie czegoś takiego „łatwego” i automatycznego nigdy nie buduje tego dodatkowego poziomu zrozumienia działania rzeczy, co jest nie tylko ważne dla ogólnej wydajności, ale w zasadzie eliminuje możliwość niepożądanego zachowania, którego przyczyny może być trudno określić . Wszystko, co stało się niejawnie możliwe dzięki COW, jest również łatwe do osiągnięcia bezpośrednio i jest bardziej jasne.
dtech,
Moje pytanie było motywowane dylematem, czy domyślnie podać COW w języku, nad którym pracuję. Po zważeniu argumentów za i przeciw, postanowiłem nie mieć go domyślnie, ale jako modyfikator, który można zastosować zarówno do nowych, jak i już istniejących typów. Wydaje się, że jest to najlepszy z obu światów, nadal możesz mieć nieskrępowane znaczenie COW, kiedy wyraźnie wyrażasz chęć.
dtech,
@ddriver To, co mamy, jest czymś podobnym do języka programowania z paradygmatem węzłowym, z wyjątkiem prostoty, jaką jest rodzaj semantyki wartości użytkowej i brak semantyki typu referencyjnego (być może nieco podobnej do std::vector<std::string>wcześniejszej wersji emplace_backsemantyki w C ++ 11) . Ale w zasadzie używamy również instancji. System węzłów może modyfikować dane lub nie. Mamy takie elementy, jak węzły tranzytowe, które nic nie robią z danymi wejściowymi, ale po prostu wypisują kopię (są one dla organizacji użytkownika jego programu). W takich przypadkach wszystkie dane są płytko kopiowane dla złożonych typów ...
@ddriver Nasze kopiowanie przy zapisie jest faktycznie procesem kopiowania typu „uczyń instancję unikalną przy zmianie” . Uniemożliwia modyfikację oryginału. Jeśli obiekt Azostanie skopiowany i nic nie zostanie zrobione, aby się sprzeciwić B, jest to tania płytka kopia dla złożonych typów danych, takich jak siatki. Teraz, jeśli zmodyfikujemy B, dane, które zmodyfikujemy, Bstaną się unikalne przez COW, ale Apozostaną nietknięte (z wyjątkiem niektórych liczb atomowych).