Заслуги семантики копіювання на запис


10

Мені цікаво, які можливі достоїнства має копіювання при написанні? Звичайно, я не чекаю особистої думки, але реальні практичні сценарії, де це може бути технічно і практично корисно в відчутному плані. І під відчутним я маю на увазі щось більше, ніж врятувати вас набравши &персонажа.

Для уточнення це питання знаходиться в контексті типів даних, де присвоєння або побудова копії створює неявну дрібну копію, але модифікації до неї створюють неявну глибоку копію і застосовують зміни до неї замість оригінального об'єкта.

Причину, про яку я питаю, - я, здається, не знаходжу жодних достоїнств того, що COW є неявною поведінкою за замовчуванням. Я використовую Qt, для якого COW реалізований для багатьох типів даних, практично для всіх, що мають деякі основні динамічно розподілені сховища. Але як це справді приносить користь користувачеві?

Приклад:

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"

Що ми виграємо, використовуючи COW у цьому прикладі?

Якщо все, що ми маємо намір - це використовувати const-операції, s1це зайве, можливо, також використовувати s.

Якщо ми маємо намір змінити значення, то COW затримує лише копію ресурсу до першої операції, що не вимагає const, за (хоча і мінімальною) вартістю збільшення кількості посилань для неявного спільного використання та від'єднання від спільного сховища. Це виглядає так, що всі накладні витрати на КРС безглузді.

Це не сильно відрізняється в контексті передачі параметрів - якщо ви не збираєтесь змінювати значення, передайте як посилання const, якщо ви хочете змінити, ви або зробите неявну глибоку копію, якщо не хочете змінювати оригінальний об'єкт або перейдіть за посиланням, якщо ви хочете його змінити. Знову COW здається непотрібним накладними, що нічого не досягає, і лише додає обмеження, що ви не можете змінювати початкове значення, навіть якщо хочете, оскільки будь-яка зміна відмежуватиметься від початкового об'єкта.

Тож залежно від того, чи знаєте ви про КОР або не звертаєте на це уваги, це може призвести або до коду з неясним наміром і непотрібним накладним навантаженням, або зовсім заплутаною поведінкою, яка не відповідає очікуванням, і залишає вас дряпати голову.

Мені здається, що є більш ефективні і читабельні рішення, чи потрібно уникати зайвої глибокої копії, чи ви збираєтесь її зробити. Тож де практична користь від корова? Я припускаю, що має бути якась користь, оскільки вона використовується в таких популярних і потужних рамках.

Крім того, з того, що я читав, COW зараз явно заборонено в стандартній бібліотеці C ++. Не знаю, чи є бачимо, що я бачу в ньому, щось спільне з цим, але в будь-якому випадку, в цьому повинна бути причина.

Відповіді:


15

Копія при записі використовується в ситуаціях, коли ви дуже часто створюєте копію об'єкта і не змінюєте її. У тих ситуаціях вона окупається.

Як ви вже згадували, ви можете передавати об'єкт const, і в багатьох випадках цього достатньо. Однак const лише гарантує, що абонент не може його вимкнути (якщо const_cast, звичайно, вони). Він не обробляє багатопотокові випадки і не обробляє випадки, коли є зворотні виклики (які можуть мутувати вихідний об'єкт). Передача об'єкта COW за значенням ставить завдання управління цими деталями розробнику API, а не користувачеві API.

Нові правила щодо С + 11 забороняють ТОП std::stringзокрема. Ітератори в рядку повинні бути визнані недійсними, якщо резервний буфер від'єднаний. Якщо ітератор реалізовувався як char*(На відміну від a string*та індексу), цей ітератор більше не діє. Спільнота C ++ повинна була вирішити, як часто ітератори можуть бути визнані недійсними, і рішення було таким, що operator[]не повинно бути одним із таких випадків. operator[]на std::stringповернення a char&, яке може бути змінено. Таким чином, operator[]потрібно буде від'єднати рядок, визначивши недійсними ітератори. Це вважалося поганою торгівлею, і на відміну від функцій на кшталт end()і cend(), немає ніякого способу вимагати версії const operator[]короткості const введення рядка. ( споріднене ).

COW все ще живий і здоровий поза STL. Зокрема, я вважаю це дуже корисним у тих випадках, коли користувачеві моїх API нерозумно розраховувати, що за предметом, що здається, дуже легким, є якийсь важкий предмет. Я, можливо, захочу використовувати COW у фоновому режимі, щоб переконатися, що вони ніколи не повинні стосуватися таких деталей щодо впровадження.


Мутація однієї струни в декількох потоках здається дуже поганим дизайном, незалежно від того, використовуєте ви ітератори чи []оператора. Тож COW забезпечує поганий дизайн - це не здається великою користю :) Точка в останньому абзаці видається дійсною, але я сам не є великим прихильником неявної поведінки - люди, як правило, сприймають це як належне, а потім мають важко розібратися, чому код не працює, як очікувалося, і продовжувати цікавитись, поки вони не з’ясують, щоб перевірити, що ховається за неявною поведінкою.
dtech

Що стосується точки використання, const_castсхоже, вона може зламати COW так само легко, як і може зламати, проходячи через посилання const. Наприклад, QString::constData()повертаючи const QChar *- const_castщо і COW згортається - ви будете мутувати вихідні дані об'єкта.
dtech

Якщо ви можете повернути дані від COW, ви повинні від'єднати їх перед тим, як зробити це, або повернути дані у формі, яка все ще відома COW ( char*очевидно, це не відомо). Що стосується неявної поведінки, я думаю, ти маєш рацію, з цим є проблеми. Дизайн API - це постійний баланс між двома крайнощами. Занадто неявно, і люди починають покладатися на особливу поведінку так, ніби вона фактично є частиною специфікації. Занадто явний, і API стає занадто громіздким, оскільки ви виставляєте занадто багато базових деталей, які насправді не були важливими, і їх раптом записують у вашу специфікацію API.
Корт Аммон

Я вважаю, що stringкласи мали поведінку COW, оскільки дизайнери-компілятори помітили, що велика частина коду копіює рядки, а не використовує const-reference. Якби вони додали COW, вони могли б оптимізувати цей випадок і зробити більше людей щасливими (і це було законно, до C ++ 11). Я ціную їхню позицію: хоча я завжди передаю свої рядки за посиланням const, я бачив усе те синтаксичне сміття, яке просто зменшує читабельність. Я ненавиджу писати const std::shared_ptr<const std::string>&лише для того, щоб зафіксувати правильну семантику!
Корт Аммон

5

Для рядків і таких, схоже, це песимізувало б більш поширені випадки використання, ніж ні, так як звичайний випадок для рядків часто є невеликими рядками, і там накладні витрати COW, як правило, значно перевищують вартість простого копіювання невеликого рядка. Невелика оптимізація буфера має для мене набагато більше сенсу, щоб уникнути розподілу купи в таких випадках замість рядкових копій.

Якщо у вас є об’єкт heftier, як-от андроїд, і ви хотіли скопіювати його та просто замінити його кібернетичну руку, COW видається цілком розумним як спосіб зберегти мутаційний синтаксис, уникаючи необхідності глибокої копіювання всього андроїда лише для надайте копії унікальну руку. Зробити це просто непорушним, оскільки стійка структура даних у цей момент може бути кращою, але "часткова КОР", застосована до окремих андроїдних частин, здається розумною для цих випадків.

У такому випадку дві копії андроїда ділиться / застосовуватимуть один і той же тулуб, ноги, стопи, голову, шию, плечі, таз і т. Д. Єдиними даними, які між ними будуть різними, а не спільними - це рука, яка була зроблена унікальний для другого андроїда при перезаписуванні руки.


Це все добре, але воно не вимагає КОР, і все ще піддається безлічі шкідливих наслідків. Крім того, в цьому є і його мінус - ви, можливо, часто хочете робити об'єкт інстанції, і я не маю на увазі введення екземпляра, а копіювати об’єкт як екземпляр, таким чином, коли ви змінюєте вихідний об'єкт, копії також оновлюються. COW просто виключає таку можливість, оскільки будь-яка зміна "спільного" об'єкта відлучає його.
dtech

Коректність ІМО не повинна бути «легкою» для досягнення, не за умови неявної поведінки. Хорошим прикладом коректності є коректність CONST, оскільки вона явна і не залишає місця для двозначностей або невидимих ​​побічних ефектів. Маючи щось на зразок цього "легкого" та автоматичного, ніколи не створює додатковий рівень розуміння того, як все працює, що не тільки важливо для загальної продуктивності, але в значній мірі виключає можливість небажаної поведінки, причину якої може бути важко визначити . Все, що стало можливим неявно за допомогою КРВ, легко досягти і явно, і це більш зрозуміло.
dtech

Моє запитання мотивовано дилемою, чи надавати COW за замовчуванням мовою, на якій я працюю. Зваживши профі та проти, я вирішив не мати його за замовчуванням, але як модифікатор, який можна застосувати як до нових, так і до вже існуючих типів. Здається, що найкраще з обох світів, ви все одно можете мати неявність КРВ, коли ви чітко бажаєте цього.
dtech

@ddriver У нас є щось, що схоже на мову програмування з вузловою парадигмою, за винятком простоти, вузли мають значення семантики використання значень і відсутність семантики опорного типу (можливо, дещо схожа до того, std::vector<std::string>як ми мали emplace_backта переміщували семантику в C ++ 11) . Але ми також в основному використовуємо інстанції. Вузольна система може або не може змінювати дані. У нас є такі речі, як прохідні вузли, які нічого не роблять із введенням, а просто виводять копію (вони є для організації користувача його програми). У цих випадках всі дані дрібно копіюються для складних типів ...

@ddriver Наш процес копіювання при записі - це фактично унікальний екземпляр, який неявно впливає на зміну . Це унеможливлює зміну оригіналу. Якщо об'єкт Aскопійовано і нічого не робиться для його заперечення B, це дешева неглибока копія для складних типів даних, таких як сітки. Тепер, якщо ми модифікуємо B, дані, які ми модифікуємо, Bстають унікальними через COW, але Aне торкаються (за винятком деяких атомних підрахунків).
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.