Что такое copy on write
Что не так с Copy-on-Write под Linux при копировании
Предупреждение: эта статья относится ко всем CoW файловым системам в Linux, поддерживающим reflink при копировании. В данный момент это: BTRFS, XFS и OCFS2.
Прошу воздержаться от холиваров о том, какая ФС лучше: Btrfs, XFS, Reiser4, NILFS2, ZFS или какая-то неупомянутая.
Предыстория
Ожидания от копирования в CoW-файловых системах
Когда в 2001 году анонсировалась Reiser4, я был вдохновлён и увлечён возможностями Copy-on-Write. Подумать только, мы можем легко и просто иметь сколько угодно копий разных проектов, а физически диск будет хранить только отличия между ними!
К тому же скорость копирования должна была неприлично вырасти. За счёт того, что при копировании создавалась бы только reflink-ссылка на старый файл. При записи в такой новый файл автоматически выделялись бы сектора для изменённых данных. В итоге мы бы имели одинаковые сектора для общих частей файлов, а разные части были бы записаны в разных секторах.
Это тогда выглядело панацеей для создания аккаунтов для shared-хостинга, а сейчас оптимальным решением для лёгких виртуальных машин и контейнеров. Ведь мы могли бы не тратить место на одинаковые файлы, вместе с тем позволяя пользователям их беспроблемно менять.
ZFS была опубликована под несовместимой с Linux лицензией, и потому не вошла в ядро. Поэтому внедрение ZFS в Linux оказалось надолго заторможено.
После этого я стал ждать Btrfs. И только через 6 лет после анонса она была признана разработчиками ядра Linux стабильной.
После этого я не торопился использовать Cow-системы, так как сама парадигма Copy-on-Write предполагает повышенную фрагментацию, потому что изменения данных каждый раз записываются в новое место.
Для HDD фрагментация убивает производительность, так как процесс перепозиционирования блока считывающих головок — очень длительная операция.
Поэтому лично я откладывал внедрение btrfs на своих машинах, пока они не перешли на SSD.
Замечу, что SSD тоже не любят фрагментацию, всем известно, что для SSD линейная запись/чтение могут быть в десятки раз быстрее, чем случайный доступ.
Но производительность фрагментированного SSD падает не так драматически, как у HDD.
Итак, что со скоростью копирования при CoW?
И вот, наконец, настал тот час. Когда SSD стали достаточно надёжны, я стал вовсю использовать CoW-файловые системы. А конкретнее — Btrfs и Nilfs2.
Осваивая захватывающие возможности снимков (снепшотов), на время я забыл о моих ожиданиях из 2000-х годов о сверхбыстром копировании файлов.
Через некоторое время я решил провести тесты. И к моему великому разочарованию не увидел никакого прироста скорости от CoW при копировании. Вот результат на SATA SSD:
Оказалось, что для использования CoW нужно указывать специальный ключ.
Я был несказанно удивлён, почему ключевое преимущество CoW-файловой системы не используется по умолчанию. Ведь для этого она и создавалась!
В данном случае я тестировал копирование на Btrfs-разделе. Но подобный результат вы получите и с любой другой CoW-файловой системой, поддерживающей reflink.
В чём проблема, Билли?
Проблема в cp. По умолчанию она не использует CoW при копировании. Хотя может.
Почему разработчиками coreutils было принято столь неоднозначное решение, которое перечеркнуло половину преимуществ CoW файловых систем?
Оказывается, так решил Pádraig Brady, ответственный за развитие GNU coreutils.
В плане скорости для mv практически нет никакой пользы от CoW при перемещении файлов. В пределах одной файловой системы mv практически всегда работает очень быстро.
Опять возникает вопрос о влиянии личности на историю. Поставив по-другому несколько буковок в исходном коде программы, можно замедлить копирование у десятков/сотен миллионов пользователей (тут нужно учесть, что сейчас большинство людей пользуются облачными сервисами в том или ином виде, даже если у них нет компьютера), уменьшить эффективность использования накопителей и повысить их продажи по всему миру.
Разбор аргументов Pádraig
В первом аргументе есть кое-какой смысл. Неискушенные пользователи действительно могут делать бэкапы ценных файлов на одной и той же файловой системе.
Второй аргумент. Если почитать дальнейшие комментарии насчёт лагов от Pádraig, то обнаружится, что он имел в виду ситуации с базами данных, когда при записи в существующий файл может возникнуть задержка из-за того, что файловая система будет искать свободное место. Но в случае CoW-файловой системы всегда будут искаться новые сектора для записи в силу природы CoW, как заметил Jan Kanis. Поэтому, по моему мнению, второй аргумент несостоятелен.
Однако на CoW системах действительно можно получить задержку или даже ошибку “Закончилось пространство” при записи в файл базы данных. Для избежания этого нужно для базы изначально создавать пустой каталог с отключенным CoW.
Отключается CoW на каталоге/файле так:
Есть ещё опция монтирования nodatacow. Она будет применена ко всем новосоздаваемым файлам.
Как всё-таки быть, если мы хотим по умолчанию использовать CoW при копировании?
В папке /etc/profile.d создаём файлик cp_reflink.sh c содержимым:
Это решение будет работать почти во всех случаях, когда к cp идёт обращение из оболочки по имени. Но если в скриптах будет использоваться /bin/cp, то алиас не сработает и копирование будет происходить по старинке.
Дистрибутивы Linux c поддержкой reflink
Файловые менеджеры и reflink
Состояние на 31 октября 2019 года:
Языки программирования, системные вызовы и reflink
В большинстве языков программирования поддержка reflink отсутствует.
На Си многие программисты до сих пор копируют с помощью циклов и буферов.
Системный вызов sendfile не использует reflink.
cp использует системный вызов ioctl с флагом FICLONE.
Выводы
С наступлением эпохи повсеместной виртуализации и SSD стало очень актуально использовать CoW-файловые системы. Они удобны для создания снимков, быстрого копирования. Фактически, при использовании CoW при копировании мы автоматически делаем дедупликацию данных.
Сейчас только 3 файловых системы поддерживают этот тип копирования: BTRFS, XFS и OCFS2.
Я искренне надеюсь, что поддержку reflink допилят в ZFS и NILFS2, так как по внутренним механизмам они и так поддерживают CoW.
Однако во всех дистрибутивах Linux CoW при копировании файлов отключена, и нам нужно или явно указывать соответствующие ключи, или использовать различные трюки типа алиасов или патчей.
С момента анонса Reiser4 прошло уже 18 лет, однако до сих пор лёгкое CoW копирование не вошло в нашу жизнь повсеместно.
P.S. Docker и CoW
А вы знаете что Docker поддерживает btrfs для своего хранилища? Эту опцию нужно включать. Она не выбрана по умолчанию.
CoW файловая система в теории является идеальным дополнением к лёгкой виртуализации, когда разные виртуальные машины используют одно и то же ядро.
На мой взгляд, гораздо более органично, чем OverlayFS и Aufs, которые представляют из себя технологические костыли для имитации CoW.
Для использования Btrfs в Docker нужно:
А вот полная инструкция для всех дистрибутивов.
Дополнения из комментариев
XFS и CoW
MacOS и CoW
Поддерживается начиная с OS X 10.12 (Sierra) и только в файловой системе APFS.
Благодарности:
Вы можете поэкспериментировать с CoW-файловыми системами, заказав виртуальную машину у RUVDS по купону ниже.
Copy-on-write в PHP
Copy-on-write или копирование при записи — один из способов управлением памятью. Но перед тем как давать какие-то определения, предлагаю рассмотреть пример:
В данном примере есть функция handle. В эту функцию передаётся массив большого размера. По умолчанию в PHP передача аргументов происходит по значению. Это означает, что если изменить значение аргумента внутри функции, то вне функции значение всё равно останется прежним. Другими словами внутри функции используется копия переменной, но для создания копии требуется выделить память.
Вопрос: в целях оптимизации стоит ли передать аргумент по ссылке handle(array &$array)?
На самом деле ответ зависит от того, что происходит внутри функции handle.
Чтение аргументаМодификация аргумента
В данном случае произойдет копирование переменной, то есть создание нового контейнера zval с выделением памяти.
Copy-on-write
Суть подхода сopy-on-write или копирование при изменении заключается в том, что при чтении переменных используется общая копия, в случае изменения переменной — создается новая копия.
Замеры памяти
Проведём тест, попробуем увидеть, что при чтении аргумента используется общая копия, а при модификации выделяется память.
Из результата видно, что при изменении аргумента, была выделена память, а при чтении нет.
Передача объекта в качестве аргумента
Стоит рассмотреть случай передачи объекта в качестве аргумента. И посмотреть, применяется ли для данного случая механизм copy-on-write. Рассмотрим следующий пример:
Складывается впечатление, что объект передаётся по ссылке, а не по значению, но это не совсем верно. На самом деле при передаче объекта в качестве аргумента передаётся только ID объекта. Содержимое объекта хранится отдельно и доступ можно получить по ID. Из-за этого, если что-то изменить внутри объекта, то это доступно и внутри функции и вне функции. Более детально можно прочитать документации PHP: Объекты и ссылки.
То есть в данном примере не нужно передавать объект по ссылке, так как передается всего лишь ID объекта.
Вывод
В подавляющем большинстве не нужно передавать аргумент по ссылке. Так как редко оперируем большими по памяти переменными. И аргументы в большинстве случаев используются только на чтение. Если речь идет про передачу объектов, то и в этом случае не нужно передавать аргумент по ссылке, так как передаётся только идентификатор объекта.
Другими словами не нужно сейчас бежать, и срочно что-то менять в вашей коде и в вашем подходе. Продолжаем писать код как, обычно, но уже более осознанно.
Копирование при записи
Из Википедии — свободной энциклопедии
Механизм копирования при записи (англ. Copy-On-Write, COW ) используется для оптимизации многих процессов, происходящих в операционной системе, таких как, например, работа с оперативной памятью или файлами на диске (пример — ext3cow).
Идея подхода copy-on-write заключается в том, что при чтении области данных используется общая копия, в случае изменения данных — создается новая копия.
Например, при работе UNIX-функции fork() вместо реального копирования, под которое надо выделять память, ядро меняет дескрипторы страниц памяти материнского процесса, запрещая какую-либо запись в страницы данных (страницы программного кода и так запрещены для записи; хотя здесь имеются свои тонкости — но на дальнейшие рассуждения они не влияют). Затем создаётся дочерний процесс, которому копируются дескрипторы страниц памяти материнского процесса. При этом ядро помечает эти страницы как совместно используемые.
Попытка записи в отображённые страницы (неважно, со стороны материнского или дочернего процесса) вызывает исключение (exception), которое передаёт управление в ядро. Ядро видит, что это обращение было законным, и создаёт копию изменяемой страницы. Таким образом удаётся снизить количество потребляемой программами физической памяти. Механизм COW достаточно сложен в реализации, особенно в многоядерных системах, а ошибки в нём могут приводить к уязвимостям, например, Уязвимость Dirty COW (Linux, 2007-октябрь 2016)
Механизм получил большое распространение при создании новейших файловых систем, таких как ZFS и Btrfs. Благодаря ему создание снимков в данных системах происходит практически мгновенно, не занимая при этом больших ресурсов носителя информации.
О бедном C++ API замолвите словцо!
Present perfect coninuous
Итак, на дворе 2014 год. Язык C++ (в отличие от его предка-подмножества языка Си) обладает всем потенциалом языка высокого уровня, который уже на протяжении многих лет остаётся не более чем потенциалом. Если разработчику не хватает чего-то высокоуровневого, придётся либо изобретать что-то своё, либо пытаться найти готовое решение, благо источников готового кода более чем предостаточно. Это и старый-добрый STL, который в C++11 немного заштопали и обновили, это и монструозный Boost, с разношёрстными библиотеками разной степени готовности, есть и Qt с его интерфейсными и не только плюшками, а также море свободно распространяемого софта с разной степени заразности лицензиями. Я не буду сравнивать богатство библиотек C++ с библиотеками для языков Java и Python, всё-таки языки решают разные задачи, но каждому очевидно, что возможности написать на C++ что-то высокоуровневое без жёсткого порно траты лишнего времени не выйдет. Итак, рано или поздно, определив свою нишу, любой разработчик С++ пишет для себя или для общества, возможно для коллег, некий общий функционал, объединяя разношёрстное разноцветия множества API в единую стройную структуру, в поиска своего дзена проектирования API. Бывают конечно люди, предпочитающие только пользоваться библиотеками, написав код, за который им платят, не интересуясь красотой построения программного интерфейса, променяв его на мирскую суету, но речь сейчас не о них… Эта статья для тянущихся к свету истинного проектирования прекрасного API языка C++, ну и немного для тех, кого к этому свету насильственно притягивают за уши.
Классы
Итак, что мы здесь видим? Конструктор по умолчанию, который порой суров к POD-типам, здесь генерируется вполне сносный, поскольку поля — стандартные контейнеры STL, у них с инициализацией по умолчанию всё в порядке. В целом вы должны иметь в виду, что конструкторы имеют свойство генерироваться: конструктор по умолчанию, конструктор копирования и в C++11 ещё и конструктор перемещения, также сгенерируются оператор копирования и для C++11 оператор перемещения.
И если для инициализации и перемещения здесь всё чисто благодаря STL, то вот с копированием благодаря тем же контейнерам STL мы получим ад рекурсивного копирования.
Простая операция:
может привести к адской головной боли несчастного разработчика, использующего ваш класс person. Представьте что в переменной granny находится построенный объект некой бабушки с богатым потомством, у неё есть куча детей и по каждому из детей ещё и на порядок больше внуков. По всем правилам все эти замечательные потомки бабушки начнут клонироваться как амёбы в объект neighbour. Так будет конечно же безопасно для бабушки, в плане дзена С++ и контейнеров STL, но совсем небезопасно для её психики, да и для оптимизации выполнения базовых операций при работе с вашим классом тоже очень и очень плохо.
Нерадивый разработчик скажет: «ой, да ладно, разберутся, не маленькие», и несчастные пользователи библиотеки будут думать, как же забороть тот самый вектор внутри каждого объекта, который рекурсивно усложняет проблему неявного копирования. И скорее всего найдут другую библиотеку, которая и спроектирована лучше, и работает более прозрачно и предсказуемо.
Неожидаемое поведение при очевидных операциях — это головная боль разработчика функционала и проектировщика API, а уж никак не несчастного пользователя их библиотеки. В крайнем случае опытные разработчики обойдут эту проблему, помещая такие опасные классы в обёртки с умными указателями, но в целом так делать нельзя. Давайте вылечим бабушку и её потомков от синхронного амёбного деления. Не оставлять же её тут в таком виде!
Итак бабушка перестала копироваться. Совсем. Что тоже не совсем правильно в концепции C++, с этим мы отдельно разберёмся. В принципе можно взять за основу Java-style и все подобные суровые значения передавать по ссылке и только по ссылке, но это значит обмануть пользователя вашей библиотеки в ситуации, когда его можно и не обманывать. Что нам мешает копировать данные этой бабушки только при изменении данных?
Copy-on-write
Метод копирования при изменении довольно прост и его довольно быстро можно реализовать самому, причём потокобезопасно (при условии потокобезопасности реализации std::shared_ptr стандартной библиотеки C++). Суть метода C-o-w в следующем, пока объект передаётся в методы как const this, данные разделяются между всеми копиями объекта порождённого от одних и тех же данных. Однако как только this становится неконстантным, любой из методов (бывают правда исключения) не помеченный как const в первую очередь проверяет std::shared_ptr::is_unique() и если на одни и те же данные ссылается больше одного объекта, мы отцепляем себе свою уникальную копию от общих данных и её правим. В принципе можно обойтись даже и без мьютекса, в худшем случае лишний раз скопируем объект, что не смертельно для тестового примера при объяснении данной темы. Реализуется же данный механизм проще всего через перегрузку operator-> для const и не-const случая this объекта промежуточного шаблонного класса, шаблонного, поскольку механизм общий. Выглядит этот промежуточный шаблон примерно так:
Всё что осталось, дать нашему классу person возможность жить в своё удовольствие, создавать безнаказанно вектора потомков person любой длины (они будут неинициализированы до первого обращения к данным), копировать, передавать по значение в функции (никаких const& не нужно, пусть это и чуть подороже), возвращать в качестве результата тоже по значению (опять же никаких const& и неявных ошибок из-за этого!) Но самое главное, что когда мы начнём изменять данные какого-либо объекта, скопируется только тот объект, который изменяют, от рекурсивного копирования останется только видимость!
Итак, person, живи полной жизнью и ни в чём себе не отказывай:
Небольшое пояснения для всех, кто ещё не понял, что изменилось для операции копирования. Дело в том что все элементы вектора-копии будут ссылаться на те же данные, что и элементы вектора-источника. Если вдруг один из элементов начнут изменять, он сразу отцепит свои данные сделав свою уникальную копию. Поэтому и копия предка не будет такой уж тяжёлой, хотя само по себе копирование вектора может иметь довольно серьёзные накладные расходы.
Константность при вызове метода
Разумеется метод выдающий ссылку на вектор — зло, крайне неудачное решение для Copy-on-write модели, особенно перегрузкой метода для const и не-const this. Просто потому что неявно может быть копирование там, где неявно используется неконстантная перегрузка. Ведь пользователь вашего API не обязан заботиться о константности своей переменной, например заводя переменную на стеке, разработчик получит неконстантную переменную. Поэтому нужно внимательно следить за тем, чтобы от константности не зависила судьба копирования объектов вашего класса. По крайней мере пусть это будет очевидно.
Итого, имеем очевидный интерфейс класса, экземпляры которого можно без проблем создавать и копировать в больших количествах, а реализацию менять хоть каждый день без изменения заголовочного файла.
Время компиляции при использовании вашего API
Бонусом получаем более шуструю компиляцию, просто потому что объявление полей person::data вынесено в отдельный файл скрытый в реализации и не нужно компилировать
В общем старайтесь заботиться о пользователях вашей библиотеки. Время компиляции также весьма важный параметр, поскольку язык C++ довольно тяжело компилируется и при неудачном проектировании API каждая пересборка при использовании вашей библиотеки может занимать на несколько порядков больше времени, чем могла и должна бы, если б учитывалась сложность компиляции и минимализация подключения избыточных заголовочных файлов. Именно поэтому не стоит злоупотреблять метапрограммированием и излишним использованием магией шаблонов, в ней вы можете поупражняться и в файлах реализации, но это уже отдельная тема, которую я вынесу в отдельную статью. А пока вернёмся к более насущной проблеме проектирования API.
Ещё пара слов о выпечке хлеба
Все кто хоть раз читал о том «Как два программиста хлеб пекли» запомнил эту статью навсегда. Несмотря на довольно забавное повествование, статья учит главному: правильно структурировать свой код, избегая лишних сущностей. Но не все понимают, что к этим лишним сущностям приводит, а факторов всего три:
1) недостаток опыта построения API (попробуйте сами его использовать, напишите хотя бы пару тестов. ну как, удобно пользоваться?)
2) перенасыщение новым материалом красивых структурированных паттернов, которые на страницах книги выглядят так заманчиво
3) преобладание энтузиазма попробовать что-то новое над устоявшимися принципами построения API, как следствие двух предыдущих пунктов.
В результате мы получаем код, где я раз за разом встречаю одни и те же ошибки проектирования.
Я бы даже сказал паттерны ошибок проектирования:
1. Странные фабрики типов, которые создают что-то заранее типа неизвестное, либо просто без основания создаётся что-то разнотипное, унаследования от одного класса-предка, обычно попытки воссоздать interface-класс из высокоуровневых языков (бывает что и без виртуального деструктора). В большинстве случаев заменяется банальным конструктором объекта-контейнера того самого интерфейса. Вместо этого над пользователем издеваются, предлагая создавать код вида:
И это заставляют делать пользователя библиотеки, вместо того, чтобы просто немного потрудиться, реализовав double dispatch, если действительно нужно наследование, но как правило хватает банального контейнера для ссылки на одного-двух наследников, указатель на предка которых можно просто поместить в private. В результате пользователь будет просто создавать обычные объекты C++ простейшим конструктором, не задумываясь о работе с каким-то указателем на интерфейс, работая с API обычного класса-контейнера.
В результате можно выполнить такой код:
Послесловие
How to implement Copy-on-Write?
I want to implement a copy-on-write on my custom C++ String class, and I wonder how to.
I tried to implement some options, but they all turned out very inefficient.
5 Answers 5
In a multi-threaded environemnt (which is most of them nowadays) CoW is frequently a huge performance hit rather than a gain. And with careful use of const references, it’s not much of a performance gain even in a single threaded environment.
Additionally, as other people have pointed out, CoW strings are really tricky to implement, and it’s easy to make mistakes. That coupled with their poor performance in threading situations makes me really question their usefulness in general. This becomes even more true once you start using C++11 move construction and move assignment.
But, to answer your question.
Here are a couple of implementation techniques that may help with performance.
First, store the length in the string itself. The length is accessed quite frequently and eliminating the pointer dereference would probably help. I would, just for consistency put the allocated length there too. This will cost you in terms of your string objects being a bit bigger, but the overhead there in space and copying time is very small, especially since these values will then become easier for the compiler to play interesting optimization tricks with.
This leaves you with a string class that looks like this:
Now, there are further optimizations you can perform. The Buf class there looks like it doesn’t really contain or do much, and this is true. Additionally, it requires allocating both an instance of Buf and a buffer to hold the characters. This seems rather wasteful. So, we’ll turn to a common C implementation technique, stretchy buffers:
When you do things this way, you can then treat data_->data_ as if it contained alloclen_ bytes instead of just 1.
Keep in mind that in all of these cases you will have to make sure that you either never ever use this in a multi-threaded environment, or that you make sure that refct_ is a type that you have both an atomic increment, and an atomic decrement and test instruction for.
There is an even more advanced optimization technique that involves using a union to store short strings right inside the bits of data that you would use to describe a longer string. But that’s even more complex, and I don’t think I will feel inclined to edit this to put a simplified example here later, but you never can tell.
I would suggest that if one wants to implement copy-on-write efficiently (for strings or whatever), one should define a wrapper type which will behave as a mutable string, and which will hold both a nullable reference to a mutable string (no other reference to that item will ever exist) and a nullable reference to an «immutable» string (references to which will never exist outside things that won’t try to mutate it). Wrappers will always be created with at least one of those references non-null; once the mutable-item reference is ever set to a non-null value (during or after construction) it will forever refer to the same target. Any time both references are non-null, the immutable-item reference will point to a copy of the item that was made some time after the most recent completed mutation (during a mutation, the immutable-item reference may or may not hold a reference to a pre-mutation value).
To read an object, check whether the «mutable-item» reference is non-null. If so, use it. Otherwise, check whether the «immutable-item» reference is non-null. If so, use it. Otherwise, use the «mutable item» reference (which by now will be non-null).
To mutate an object, check whether the «mutable-item» reference is non-null. If not, copy the target of the «immutable item» reference and CompareExchange a reference to the new object into the «mutable item» reference. Then mutate the target of the «mutable item» reference and invalidate the «immutable item» reference.
To clone an object, if the clone is expected to be cloned again before it is mutated, retrieve the value of the «immutable-item» reference. If it is null, make a copy of the «mutable item» target and CompareExchange a reference to that new object into the immutable-item reference. Then create a new wrapper whose «mutable-item» reference is null, and whose «immutable-item» reference is either the retrieved value (if it wasn’t null) or the new item (if it was).
To clone an object, if the clone is expected to be mutated before it is cloned, retrieve the value of the «immutable-item» reference. If null, retrieve the «mutable-item» reference. Copy the target of whichever reference was retrieved and create a new wrapper whose «mutable-item» reference points to the new copy, and whose «immutable-item» reference is null.
The two cloning methods will be semantically identical, but picking the wrong one for a given situation will result in an extra copy operation. If one consistently chooses the correct copy operation, one will get most of the benefit of an «aggressive» copy-on-write approach, but with far less threading overhead. Every data holding object (e.g. string) will either be unshared mutable or shared immutable, and no object will ever switch between those states. Consequently, one could if desired eliminate all «threading/synchronization overhead» (replacing the CompareExchange operations with straight stores) provided that no wrapper object is used in more than one thread simultaneously. Two wrapper objects might hold references to the same immutable data holder, but they could be oblivious to each others’ existence.
Note that a few more copy operations may be required when using this approach than when using an «aggressive» approach. For example, if a new wrapper is created with a new string, and that wrapper is mutated, and copied six times, the original wrapper would hold references to the original string holder and an immutable one holding a copy of the data. The six copied wrappers would just hold a reference to the immutable string (two strings total, although if the original string were never mutated after the copy was made, an aggressive implementation could get by with one). If the original wrapper were mutated, along with five of the six copies, then all but one of the references to the immutable string would get invalidated. At that point, if the sixth wrapper copy were mutated, an aggressive copy-on-write implementation might realize that it held the only reference to its string, and thus decide a copy was unnecessary. The implementation I describe, however, would create a new mutable copy and abandon the immutable one. Despite the fact that there are some extra copy operations, however, the reduction in threading overhead should in most cases more than offset the cost. If the majority of logical copies that are produced are never mutated, this approach may be more efficient than always making copies of strings.