Преимущества семантики копирования при записи

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 в этом примере?

Если все, что мы собираемся сделать, это использовать константные операции, s1это избыточно, может также использоваться s.

Если мы намереваемся изменить значение, то COW только задерживает копирование ресурса до первой неконстантной операции, за счет (хотя и минимальной) стоимости увеличения числа ссылок для неявного совместного использования и отключения от общего хранилища. Похоже, что все накладные расходы, связанные с COW, бессмысленны.

Он не сильно отличается в контексте передачи параметров - если вы не собираетесь изменять значение, передайте как константную ссылку, если вы хотите изменить, вы либо создаете неявную глубокую копию, если не хотите изменять исходный объект, или передайте по ссылке, если вы хотите изменить его. Опять же, COW кажется ненужными накладными расходами, которые ничего не достигают, а лишь добавляет ограничение, что вы не можете изменять исходное значение, даже если хотите, так как любое изменение отсоединит от исходного объекта.

Таким образом, в зависимости от того, знаете ли вы о COW или не обращаете на это внимания, это может привести либо к коду с неясными намерениями и ненужными накладными расходами, либо к полностью запутывающему поведению, которое не соответствует ожиданиям и заставляет вас ломать голову.

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

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

Dtech
источник

Ответы:

15

Копирование при записи используется в ситуациях, когда вы очень часто создаете копию объекта, а не изменяете ее. В таких ситуациях это окупается.

Как вы упомянули, вы можете передать объект const, и во многих случаях этого достаточно. Тем не менее, const только гарантирует, что вызывающая сторона не может изменить его (если они const_cast, конечно, не). Он не обрабатывает случаи многопоточности и не обрабатывает случаи, когда существуют обратные вызовы (которые могут изменить исходный объект). Передача объекта COW по значению ставит задачи управления этими деталями разработчику API, а не пользователю API.

Новые правила для C + 11 запрещают COW std::stringв частности. Итераторы в строке должны быть недействительными, если резервный буфер отключен. Если итератор был реализован как a char*(в отличие от a string*и индекса), эти итераторы больше не действительны. Сообщество C ++ должно было решить, как часто итераторы могут быть признаны недействительными, и было решено, что это operator[]не должно быть одним из таких случаев. operator[]на a std::stringвозвращает char&, который может быть изменен. Таким образом, operator[]потребуется отсоединить строку, лишив законной силы итераторы. Считалось, что это плохая сделка, и в отличие от таких функций, как end()и cend(), нет никакого способа запросить константную версию operator[]короткого замыкания на константное приведение строки. ( связано ).

COW все еще жив и далеко за пределами STL. В частности, я считаю, что это очень полезно в тех случаях, когда пользователю моих API нецелесообразно ожидать, что за каким-то очень легким объектом стоит какой-то тяжеловесный объект. Я могу пожелать использовать COW в фоновом режиме, чтобы они никогда не интересовались такими деталями реализации.

Корт Аммон
источник
Мутирование одной и той же строки в нескольких потоках кажется очень плохим дизайном, независимо от того, используете ли вы итераторы или []оператор. Таким образом, COW допускает плохой дизайн - это не кажется большой выгодой :) Пункт в последнем параграфе кажется верным, но я сам не большой поклонник неявного поведения - люди склонны принимать это как должное, а затем трудно понять, почему код работает не так, как ожидалось, и все время удивляться, пока они не выяснят, что скрывается за скрытым поведением.
Dtech
Что касается точки использования, то const_castкажется, что она может сломать COW так же легко, как и прервать передачу по константной ссылке. Например, QString::constData()возвращает const QChar *- const_castто и COW рушится - вы будете мутировать данные исходного объекта.
Dtech
Если вы можете вернуть данные из COW, вы должны либо отключить их, либо вернуть данные в форме, которая все еще знает COW ( char*очевидно, не знает). Что касается неявного поведения, я думаю, что вы правы, с этим есть проблемы. Дизайн API - это постоянный баланс между двумя крайностями. Слишком неявный, и люди начинают полагаться на особое поведение, как если бы оно было де-факто частью спецификации. Слишком явный, и API становится слишком громоздким, так как вы раскрываете слишком много базовых деталей, которые на самом деле не были важны и внезапно вписываются в вашу спецификацию API.
Корт Аммон
Я полагаю, что stringклассы получили поведение COW, потому что разработчики компилятора заметили, что большая часть кода копирует строки, а не использует const-ссылку. Если бы они добавили COW, они могли бы оптимизировать этот случай и сделать больше людей счастливыми (и это было законно, до C ++ 11). Я ценю их позицию: хотя я всегда передаю свои строки по константной ссылке, я видел весь этот синтаксический мусор, который просто ухудшает читабельность. Я ненавижу писать const std::shared_ptr<const std::string>&только для того, чтобы захватить правильную семантику!
Корт Аммон
5

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

Однако, если у вас есть более здоровенный объект, например, андроид, и вы хотите скопировать его и просто заменить его кибернетическую руку, COW кажется вполне разумным способом сохранить изменяемый синтаксис, избегая при этом необходимости глубокого копирования всего Android только для дать копию уникального оружия. Делать его просто неизменным в качестве постоянной структуры данных в этот момент может быть лучше, но «частичное COW», применяемое к отдельным частям Android, кажется разумным для этих случаев.

В таком случае две копии андроида будут иметь один и тот же торс, ноги, ступни, голову, шею, плечи, таз и т. Д. Единственная информация, которая будет отличаться между ними и не будет использоваться, это рука, которая была сделана уникальный для второго андроида при перезаписи его руки.


источник
Это все хорошо, но это не требует COW, и все еще подвергается большому количеству вредного безобразия. Кроме того, в этом есть и недостаток - вы можете захотеть создавать экземпляры объектов, и я не имею в виду создание экземпляров типов, а копирование объекта как экземпляра, поэтому при изменении исходного объекта копии также обновляются. COW просто исключает такую ​​возможность, так как любое изменение в «общем» объекте отсоединяет его.
дтек
Правильность ИМО не должна быть «легкой» для достижения, не с неявным поведением. Хорошим примером правильности является правильность CONST, поскольку она является явной и не оставляет места для неясностей или невидимых побочных эффектов. Наличие чего-то вроде этого «простого» и автоматического никогда не создает такого дополнительного уровня понимания того, как все работает, что не только важно для общей производительности, но и в значительной степени исключает возможность нежелательного поведения, причину которого трудно определить , Все, что стало возможным неявно с COW, также легко достижимо и более понятно.
Dtech
Мой вопрос был мотивирован дилеммой, предоставлять ли COW по умолчанию на языке, над которым я работаю. Взвесив «за» и «против», я решил не иметь его по умолчанию, а как модификатор, который можно применять как к новым, так и к уже существующим типам. Похоже, лучшее из обоих миров, вы все еще можете иметь неявность COW, когда вы явно хотите этого.
дтек
@ddriver Что мы имеем что - то похожее на язык программирования с узловой парадигмой, для простоты , за исключением узлов вида использования семантики значений и семантики , ссылки типа (возможно , несколько сродни , std::vector<std::string>прежде чем мы имели emplace_backи двигаться семантика в C ++ 11) , Но мы также в основном используем инстансинг. Узловая система может изменять или не изменять данные. У нас есть такие вещи, как сквозные узлы, которые ничего не делают с вводом, а просто выводят копию (они существуют для организации пользователя его программы). В этих случаях все данные копируются для сложных типов ...
@ddriver Наш метод копирования при записи - это процесс копирования, который делает экземпляр уникальным неявно при изменении . Это делает невозможным изменение оригинала. Если объект Aкопируется и ничего не делается для объекта B, это дешевая мелкая копия для сложных типов данных, таких как сетки. Теперь, если мы модифицируем B, данные, в которые мы модифицируем, Bстановятся уникальными благодаря COW, но Aостаются нетронутыми (за исключением некоторых атомных ссылок).