При копировании значения с copy-on-write-хранилищем что происходит с его общим буфером до первой мутации?
До первой мутации копии используют один и тот же буфер, а его хранилище удерживается дополнительной сильной ссылкой. Физическое копирование данных выполняется только тогда, когда одна из копий должна быть изменена, а буфер не является уникальным.
Copy-on-write появился как способ совместить семантику копируемых значений с эффективным использованием памяти. Без него копирование больших массивов, строк или других контейнеров требовало бы немедленно дублировать весь объём данных.
В Swift это позволяет стандартным value-типам сохранять поведение независимых значений, откладывая дорогостоящую копию до момента, когда независимость действительно понадобится.
Если две копии значения сразу владеют разными буферами, копирование становится дорогим даже в сценарии, где данные никогда не изменяются. Если же обе копии всегда используют один изменяемый буфер, изменение одной копии неожиданно изменит и другую, нарушив семантику value-типа.
Поэтому требуется разделять буфер до мутации, но не раньше. Неверная реализация может привести либо к лишним аллокациям, либо к видимым побочным изменениям между логически независимыми значениями.
При копировании value-типа копируется его дескриптор, содержащий ссылку на внутреннее хранилище. Само хранилище не дублируется: ARC увеличивает число сильных ссылок на объект-хранилище.
Перед изменением контейнер проверяет, можно ли безопасно изменить буфер на месте. Если хранилище разделяется несколькими владельцами, создаётся новый буфер, данные копируются в него, и мутация выполняется уже над новой копией. Исходное значение продолжает использовать старый буфер.
ARC управляет временем жизни самого внутреннего ссылочного хранилища, но не превращает value-тип в ссылочный. Для пользователя first и second остаются независимыми значениями.
Копирование может быть отложено не только до явного метода изменения: любая операция, требующая модификации буфера, должна сначала обеспечить его уникальность. Если значение содержит элементы-ссылки, копируется буфер ссылок, но не сами экземпляры классов внутри него.
Главный компромисс — экономия памяти и времени при копировании против затрат на проверку уникальности и возможное полное копирование при мутации.
Модуль обработки изображений передаёт большой массив пикселей между несколькими этапами пайплайна. Большинство этапов только читает данные, поэтому немедленное копирование буфера создало бы высокий расход памяти.
Вариант с общим всегда изменяемым буфером быстрее, но нарушает изоляцию этапов: изменение изображения на одном этапе может повредить данные другого. Вариант с немедленным копированием безопасен, но создаёт лишние аллокации.
Выбирается copy-on-write-хранилище. Все этапы чтения используют общий буфер, а при первой модификации конкретный этап получает собственную копию. В результате сохраняются value-семантика и приемлемое потребление памяти; пиковая стоимость возникает только там, где действительно нужна независимая модификация.
Нет, обычно копируется контейнерный буфер, содержащий ссылки, а не сами объекты. После разделения две коллекции имеют разные массивы ссылок, но ссылки внутри них могут указывать на одни и те же экземпляры классов. Поэтому мутация объекта через одну коллекцию может быть видна через другую.
Общий изменяемый буфер дал бы ссылочную семантику: две переменные, логически являющиеся копиями значения, наблюдали бы изменения друг друга. Copy-on-write сохраняет общий буфер только до первой мутации и тем самым отделяет оптимизацию хранения от внешней семантики value-типа.
Нет. После того как старый буфер перестал иметь сильных владельцев, его объект становится доступным для уничтожения, но аллокатор или операционная система могут не вернуть освобождённые страницы процессу немедленно. Поэтому логическое освобождение буфера и уменьшение RSS процесса — разные события.