Начиная с C++11 COW строчки запрещены в стандарте. Там наложены специальные условия на строчку, что COW реализовать невозможно. Все современные имплементации стандартных библиотек не имеют COW строк.
COW устарел, осторожнее с его использованием.
Аккуратнее используйте COW в новых проектах. Не доверяйте статьям, которым больше 10 лет, перепроверяйте их. COW не показывает таких хороших результатов, как 20 лет назад.
В чем проблема с COW?
Там еще перед этим есть string: COW MT fixes
Например, есть два потока, в них по строке, которые ссылаются на общий динамический объект. Если потоки работают со строками и одновременно решают их удалить, получается, что мы из двух потоков будем пытаться одновременно менять динамический счетчик ссылок use_count. Это приведет к тому, что либо возникнет утечка памяти, либо приложение аварийно завершится.
Не совсем понял, а когда это строки стали thread safe?
Здравствуйте, pax123, Вы писали:
P>Не совсем понял, а когда это строки стали thread safe?
Ну вот представь себе что ты честно вызвал копирование строки, чтобы другая нитка имела свою копию.
А оно взяло и COW применило, а ты и предположить такого не мог.
Понимаешь?
The God is real, unless declared integer.
Re[2]: COW устарел, осторожнее с его использованием?
Здравствуйте, netch80, Вы писали:
P>>Не совсем понял, а когда это строки стали thread safe?
N>Ну вот представь себе что ты честно вызвал копирование строки, чтобы другая нитка имела свою копию. N>А оно взяло и COW применило, а ты и предположить такого не мог. N>Понимаешь?
Проблема на большом количестве потоков / ядер. Каждое обращение к строке — дёргает атомарную операцию, которая в свою очередь вызывает сброс кэша. На 8 ядрах может и не будет заметно, но на 128 уже да. Где-то видел бенчмарки на Qt, где практически все контейнеры реализованы через COW и там просадка очень чувствовалась.
P>Там еще перед этим есть string: COW MT fixes P>
P>Например, есть два потока, в них по строке, которые ссылаются на общий динамический объект. Если потоки работают со строками и одновременно решают их удалить, получается, что мы из двух потоков будем пытаться одновременно менять динамический счетчик ссылок use_count. Это приведет к тому, что либо возникнет утечка памяти, либо приложение аварийно завершится.
P>Не совсем понял, а когда это строки стали thread safe?
Я так понял, что тут имелась в виду самописная реализация. Несколько нетривиально сделать корректный неблокирующий контейнер. Не будешь же с каждой строкой таскать мьютекс.
Здравствуйте, pax123, Вы писали:
P>В чем проблема с COW?
Проблема с COW не имеет никакого отношения к многопоточности. Проблема с COW заключается в том, что данные в `std::string` официально являются непрерывным массивом и спецификация `std::string` разрешает бесконтрольную раздачу итераторов/указателей/ссылок на этот массив. Это запросто может приводить к возникновению "висящих" итераторов/указателей/ссылок в таком многообразии однопоточных контекстов, что пытаться специфицировать правила инвалидации таких итераторов/указателей/ссылок — бесперспективное занятие.
Best regards,
Андрей Тарасевич
Re[2]: COW устарел, осторожнее с его использованием?
Здравствуйте, SaZ, Вы писали:
P>>В чем проблема с COW?
SaZ>Проблема на большом количестве потоков / ядер. Каждое обращение к строке — дёргает атомарную операцию, которая в свою очередь вызывает сброс кэша. На 8 ядрах может и не будет заметно, но на 128 уже да. Где-то видел бенчмарки на Qt, где практически все контейнеры реализованы через COW и там просадка очень чувствовалась.
Если бы предыдущий оратор не подсказал, что потоки будут шарить одно и то же представление строки, то я бы тебя не понял, как и не понял, почему в статье эта тема поднята. А про производительность там написано
P>>Там еще перед этим есть string: COW MT fixes P>>
P>>Например, есть два потока, в них по строке, которые ссылаются на общий динамический объект. Если потоки работают со строками и одновременно решают их удалить, получается, что мы из двух потоков будем пытаться одновременно менять динамический счетчик ссылок use_count. Это приведет к тому, что либо возникнет утечка памяти, либо приложение аварийно завершится.
Да, я понял. Проблема не столько в COW как таковом, а в его применении для стандартных контейнеров. А так — вполне можно сделать COW контейнер, просто надо указать, что между потоками нужно глубокое копирование/клонирование
P>>Не совсем понял, а когда это строки стали thread safe?
SaZ>Я так понял, что тут имелась в виду самописная реализация. Несколько нетривиально сделать корректный неблокирующий контейнер. Не будешь же с каждой строкой таскать мьютекс.
Стандартные контейнеры все вроде не тред сейф
Re[2]: COW устарел, осторожнее с его использованием?
Здравствуйте, Андрей Тарасевич, Вы писали:
P>>В чем проблема с COW?
АТ>Проблема с COW не имеет никакого отношения к многопоточности. Проблема с COW заключается в том, что данные в `std::string` официально являются непрерывным массивом и спецификация `std::string` разрешает бесконтрольную раздачу итераторов/указателей/ссылок на этот массив. Это запросто может приводить к возникновению "висящих" итераторов/указателей/ссылок в таком многообразии однопоточных контекстов, что пытаться специфицировать правила инвалидации таких итераторов/указателей/ссылок — бесперспективное занятие.
Не понятно, чем в этом случае строка с COW отличается от строки без этого
Re[3]: COW устарел, осторожнее с его использованием?
Здравствуйте, pax123, Вы писали:
P>Не понятно, чем в этом случае строка с COW отличается от строки без этого
Стандарт предъявляет хитрые требования к итераторам после вызова определенных функций строки. Проще говоря, итератор гарантированно не должен меняться. В случае расщепления COW буфера, когда та или иная "голова" хочет менять данные, указатель на буфер вынужденно меняется, что противоречит стандарту.
В С++11 требования изменили, и сказали, что такой код падать не должен, даже если из-за этого он будет работать медленнее.
2. В COW-строках всё плохо с неконстантными методами
Это скорее проблема всего языка С++, что в нём нельзя заранее узнать как будет использоваться результат функции.
Например, operator[] возвращает ссылку на char. Но он не знает что с ней будут делать: только читать или ещё и записывать. Если записывать, то COW (что переводится как copy-on-write, напомню) вынуждает реализацию склонировать строку. В результате иногда COW-строки работают не так эффективно как могли бы: в них есть ложные копирования из-за того, что программист вызывал неконстантную перегрузку вместо константной.
void foo(std::string& s) {
printf("%c", s[0]); // медленно
printf("%c", std::as_const(s)[0]); // всегда быстро
}
Но расставлять const или писать везде as_const утомительно...
3.В COW-строках есть atomic, которые не очень работают на многоядерных системах
На самом деле полная фигня и упереться в это в реально программе довольно сложно. Обычно либо строки разные, либо для них достаточно быстро вызывается метод clone/detach, либо они константные и атомики не трогаются.
Упереться разве что можно, если у тебя в реализации пустая строка сделана синглтоном и существует в единственном экземпляре. Тогда все потоки с пустыми строками будут теребить этот несчастный счётчик. Впрочем в практичных реализациях COW-строк этот случай учтён: например, введением строк с глобальным временем жизни, у которых не нужно трогать счётчик, так как строка заведомо живёт дольше остальной программы. И пустая строка как раз попадает в эту категорию.
Примерно такой список причин по убыванию важности.
И да, у не-COW строк тоже свой список недостатков: нельзя сказать, что они в любом сценарии лучше.
P>Там еще перед этим есть string: COW MT fixes P>
P>Например, есть два потока, в них по строке, которые ссылаются на общий динамический объект. Если потоки работают со строками и одновременно решают их удалить, получается, что мы из двух потоков будем пытаться одновременно менять динамический счетчик ссылок use_count. Это приведет к тому, что либо возникнет утечка памяти, либо приложение аварийно завершится.
Ты же понимаешь, что это стиль написания статьи такой: сначала дать заведомо неправильную реализацию, а потом показать как этот фрагмент кода исправить?
То что написано в этом абзаце относится только к первой версии реализации строки из статьи, а не к реализациям std::string в популярных версиях STL.
P>Не совсем понял, а когда это строки стали thread safe?
Во-первых, до С++11 в языке не было вообще ничего про многопоточность. Реализации поддерживали многопоточность, все ей пользовались, но в стандарте её не было. Поэтому и не было требования thread-safe у контейнеров.
Во-вторых, у других стандартных контейнеров вполне себе есть подмножество операций, которые обязаны быть потокобезопасными (например, но не только). И со строками аналогично. Так что надо уточнять какие операции ты имеешь ввиду: некоторые потокобезопасные, другие — нет.
В-третьих, в этом фрагменте статьи идёт речь о совсем другой ситуации: не когда два потока меняют одну и ту же переменную строкового типа, а когда каждый поток меняет свою переменную, которая находится у него в эксклюзивном владении:
const int threads = 2;
std::vector<std::string> v{threads, "long shared string "};
for (int i = 0; i < threads ; ++i) {
auto fn = [i, &v]() {
v[i] += ('0' + i);
puts(v[i].c_str());
};
std::thread(fn).detach();
}
Тут каждый поток меняет только свою строку: v[0] либо v[1]. Это разные переменные к которым доступ идёт из разных потоков. Поэтому даже если бы было сказано, что ни одна операция с std::string не является thread-safe, то такой код всё равно должен работать правильно.
И в нормальных реализация COW-строк он действительно работает всегда правильно, даже не смотря на то, что все строки ссылаются изначально на один и тот же общий буфер.
Вот как например std::string устроена в libstdc++ до С++11: bits/cow_string.h — вообще никаких проблем с этим сценарием. И никаких мьютексов или спинлоков для взаимоисключения одновременного доступа там тоже нет — просто аккуратно написан код, в котором действия выполняются в порядке, который никогда не приводит в ошибочное состояние. В других версиях STL для С++03 — аналогично.
И в статье не говорится, что описанная ситуация — это какой-то фатальный сценарий для правильной работы COW-строк, в ней скорее говорится, что если ты реализуешь COW-контейнер вручную, то об этой ситуации просто нужно не забыть. Это примерно на уровне того, что при реализации MyClass::operator=(const MyClass& other) нужно тоже подумать чуть-чуть про то, что произойдёт, если сделать самоприсваивание a = a, и в каком порядке нужно выполнять действия, чтобы не уронить программу из-за того, что ресурсы this будут освобождены до чтения ресурсов из other.
Здравствуйте, pax123, Вы писали:
P>Да, я понял. Проблема не столько в COW как таковом, а в его применении для стандартных контейнеров. А так — вполне можно сделать COW контейнер, просто надо указать, что между потоками нужно глубокое копирование/клонирование
Чтобы это сделать, надо для каждой строки будет хранить thread id и запрашивать его на каждый чих. Тоже слишком оверхед. На самом деле, если нужны такие жёсткие оптимизации, то всегда можно сделать узкоспециализированными строками.
P>>>Не совсем понял, а когда это строки стали thread safe?
Я так понял, что тут имелась в виду самописная реализация. Несколько нетривиально сделать корректный неблокирующий контейнер. Не будешь же с каждой строкой таскать мьютекс.
P>Стандартные контейнеры все вроде не тред сейф
Смотря что понимать под "стандартными" и "тред сейф". В Qt это QSharedData и релевантные. Сам COW — всегда потокобезопасный, работа с элементами — нет.
Здравствуйте, pax123, Вы писали:
P>Здравствуйте, Андрей Тарасевич, Вы писали:
P>>>В чем проблема с COW?
АТ>>Проблема с COW не имеет никакого отношения к многопоточности. Проблема с COW заключается в том, что данные в `std::string` официально являются непрерывным массивом и спецификация `std::string` разрешает бесконтрольную раздачу итераторов/указателей/ссылок на этот массив. Это запросто может приводить к возникновению "висящих" итераторов/указателей/ссылок в таком многообразии однопоточных контекстов, что пытаться специфицировать правила инвалидации таких итераторов/указателей/ссылок — бесперспективное занятие.
P>Не понятно, чем в этом случае строка с COW отличается от строки без этого
Один из заезженных примеров, которые приводят в таком случае — это вызов оператора `[]`. Стандарт хочет, чтобы вызов `[]` даже для неконстантного объекта, сам по себе не приводил к расщеплению, т.е. к COPY. В частности, вот в таком примере
std::string a = "abc";
const char *ptr = a.data();
{
std::string b = a;
a[0];
}
// ...
для полноценной работы COW после вызова `a[0]` должно происходить расщепление, в результате которого объект `b` окажется единственным владельцем исходных данных. Уничтожение `b` в конце блока приведет к тому, что после завершения блока `ptr` окажется висящим указателем.
В С++03 пытались выдумывать какие-то более-менее сложные правила, призванные объяснить пользователю, что "так делать нельзя". Но потом к С++11 просто плюнули и постулировали, что такой код не имеет права инвалидировать `ptr`. В спецификации `std::string` появилось простое и прямое требование 23.4.3.2/4.2
> 4 References, pointers, and iterators referring to the elements of a basic_string sequence may be invalidated by the following uses of that basic_string object: > (4.1) Passing as an argument to any standard library function taking a reference to non-const basic_string as an argument.212 > (4.2) Calling non-const member functions, except operator[], at, data, front, back, begin, rbegin, end, and rend.
Здравствуйте, pax123, Вы писали:
P>В чем проблема с COW?
Современные строки с маленькой длиной ещё в принципе не используют динамическую память. Вроде как, таких кейсов много (адреса, логины, пароли, номера телефонов, тэги и т.д.). Поэтому проблема с производительностью, для которой нужен COW, частично решилась сама собой.
В стандарт добавили string_view, что решило ещё одну небольшую проблему с производительностью.
Для возвращаемых строк срабатывает же copy ellision? Ещё один плюс к производительности.
Получается, что COW реально может быть полезно лишь в очень узком числе кейсов, для которых можно взять и нестандартные строки.
P>Начиная с C++11 COW строчки запрещены в стандарте. Там наложены специальные условия на строчку, что COW реализовать невозможно. Все современные имплементации стандартных библиотек не имеют COW строк.
P>COW устарел, осторожнее с его использованием.
P>В чем проблема с COW?
Кстати, в Qt почти все стандартные структуры данных, как я понял, COW. При присвоении присваивается только указатель на уже имеющиеся данные. До первой операции модификации. Уже только тогда экземпляр создаёт себе личную копию данных. Я на эти грабли наступил один раз, когда очень хитрое расширение для массивов ваял, когда в чужих классах можно структуры, хранящиеся в массиве, расширить дополнительными полями, не увеличив размер структуры и не перекомпилируя чужой класс. Тогда пришлось глубже копнуть и накопалась полезная недокументированная функция detach(), которая как раз и вызывается внутри Qt для создания собственной копии данных.
Re[3]: COW устарел, осторожнее с его использованием?
Здравствуйте, pax123, Вы писали:
P>Здравствуйте, netch80, Вы писали:
P>>>Не совсем понял, а когда это строки стали thread safe?
N>>Ну вот представь себе что ты честно вызвал копирование строки, чтобы другая нитка имела свою копию. N>>А оно взяло и COW применило, а ты и предположить такого не мог. N>>Понимаешь?
P>Да, дошло
а до меня нет. Ну имеют они ссылку на одно и тоже — в чём проблема? Для программиста это будет выглядеть как копия.
Re[4]: COW устарел, осторожнее с его использованием?
Здравствуйте, sergii.p, Вы писали:
SP>а до меня нет. Ну имеют они ссылку на одно и тоже — в чём проблема? Для программиста это будет выглядеть как копия.
Проблема не в многопоточности, а в новых требованиях стандарта к интерфейсу строки.
Re[5]: COW устарел, осторожнее с его использованием?
Здравствуйте, Андрей Тарасевич, Вы писали:
АТ>Один из заезженных примеров, которые приводят в таком случае — это вызов оператора `[]`. Стандарт хочет, чтобы вызов `[]` даже для неконстантного объекта, сам по себе не приводил к расщеплению, т.е. к COPY. В частности, вот в таком примере
АТ>
АТ>для полноценной работы COW после вызова `a[0]` должно происходить расщепление, в результате которого объект `b` окажется единственным владельцем исходных данных. Уничтожение `b` в конце блока приведет к тому, что после завершения блока `ptr` окажется висящим указателем.
А нельзя для неконстантного operator[] возвращать прокси объект, который имеет оператор преобразования и оператор присваивания, и только в последнем производить расщепление строки?
>> 4 References, pointers, and iterators referring to the elements of a basic_string sequence may be invalidated by the following uses of that basic_string object: >> (4.1) Passing as an argument to any standard library function taking a reference to non-const basic_string as an argument.212 >> (4.2) Calling non-const member functions, except operator[], at, data, front, back, begin, rbegin, end, and rend.
АТ>Вот это "except..." и поставило крест на COW.
Это не решить прокси-объектами?
Re[2]: COW устарел, осторожнее с его использованием?
Здравствуйте, Nuzhny, Вы писали:
P>>В чем проблема с COW?
N>Современные строки с маленькой длиной ещё в принципе не используют динамическую память. Вроде как, таких кейсов много (адреса, логины, пароли, номера телефонов, тэги и т.д.). Поэтому проблема с производительностью, для которой нужен COW, частично решилась сама собой.
А увеличившийся довольно сильно размер объекта строки не сильно просаживает производительность?
N>В стандарт добавили string_view, что решило ещё одну небольшую проблему с производительностью.
N>Для возвращаемых строк срабатывает же copy ellision? Ещё один плюс к производительности.
N>Получается, что COW реально может быть полезно лишь в очень узком числе кейсов, для которых можно взять и нестандартные строки.
Ясно, спс
Re[4]: COW устарел, осторожнее с его использованием?
Здравствуйте, sergii.p, Вы писали:
P>>Да, дошло
SP>а до меня нет. Ну имеют они ссылку на одно и тоже — в чём проблема? Для программиста это будет выглядеть как копия.
Работа с общими данными должна быть потокобезопасна. У разных потоков есть своя "копия" строки, соответственно, они не занимаются синхронизацией. И вот они все захотят изменить строку. И тут начнутся проблемы
Re[5]: COW устарел, осторожнее с его использованием?
Здравствуйте, pax123, Вы писали:
P>Работа с общими данными должна быть потокобезопасна. У разных потоков есть своя "копия" строки, соответственно, они не занимаются синхронизацией. И вот они все захотят изменить строку. И тут начнутся проблемы
идню вроде понял. Только наверное вы имели ввиду: "Работа с разделёнными данными должна быть потокобезопасна"
Re[5]: COW устарел, осторожнее с его использованием?
Здравствуйте, pax123, Вы писали:
P>А нельзя для неконстантного operator[] возвращать прокси объект, который имеет оператор преобразования и оператор присваивания, и только в последнем производить расщепление строки?
Нельзя, это явный слом существующего API, что не допустимо.
Re[5]: COW устарел, осторожнее с его использованием?
P>А нельзя для неконстантного operator[] возвращать прокси объект, который имеет оператор преобразования и оператор присваивания, и только в последнем производить расщепление строки?
вроде без разницы. Ну будет
a[0] = 'A';
всё равно ведь ptr висит
Re[6]: COW устарел, осторожнее с его использованием?
Здравствуйте, vopl, Вы писали:
P>>А нельзя для неконстантного operator[] возвращать прокси объект, который имеет оператор преобразования и оператор присваивания, и только в последнем производить расщепление строки?
V>Нельзя, это явный слом существующего API, что не допустимо.
А что именно сломается?
Re[5]: COW устарел, осторожнее с его использованием?
Здравствуйте, pax123, Вы писали: P>А нельзя для неконстантного operator[] возвращать прокси объект, который имеет оператор преобразования и оператор присваивания, и только в последнем производить расщепление строки?
В старой книжке по C++03, то ли Мейерса, то ли Саттера, разбирался такой пример с char прокси для строки.
Re[7]: COW устарел, осторожнее с его использованием?
Здравствуйте, pax123, Вы писали:
P>Здравствуйте, vopl, Вы писали:
P>>>А нельзя для неконстантного operator[] возвращать прокси объект, который имеет оператор преобразования и оператор присваивания, и только в последнем производить расщепление строки?
V>>Нельзя, это явный слом существующего API, что не допустимо.
P>А что именно сломается?
Сломаются прикладные варианты использования этого std::string::operator[]. Другими словами — вся прикладная кодовая база, которая завязана на специфику текущего API — она перестанет работать. А на эту специфику завязано много кода..
Например, предлагаемый прокси не будет прямо или косвенно предоставлять информацию об адресе проксируемого символа (хотя бы потому что такой адрес в случае прокси теряет смысл). Но, существует немало кода, так или иначе завязанного на то что operator[] такой адрес (косвенно) предоставляет:
std::string s = "...";
char* begin = &s[0];
char* end = s.data() + s.size();
assert(end - begin == s.size());
вот такое легаси будет сломано
Re[8]: COW устарел, осторожнее с его использованием?
Здравствуйте, vopl, Вы писали:
V>Например, предлагаемый прокси не будет прямо или косвенно предоставлять информацию об адресе проксируемого символа (хотя бы потому что такой адрес в случае прокси теряет смысл). Но, существует немало кода, так или иначе завязанного на то что operator[] такой адрес (косвенно) предоставляет:
Здравствуйте, pax123, Вы писали:
P>Здравствуйте, vopl, Вы писали:
V>>Например, предлагаемый прокси не будет прямо или косвенно предоставлять информацию об адресе проксируемого символа (хотя бы потому что такой адрес в случае прокси теряет смысл). Но, существует немало кода, так или иначе завязанного на то что operator[] такой адрес (косвенно) предоставляет:
P>Почему не будет?
P>
> Кто такой Object object?
Это не важно. Сниппет демонстрирует, что в С++ можно перегружать унарный operator&.
V>Не вполне понял, что за адрес будет возвращен?
С помощью перегрузки my_char_proxy::operator& ты можешь сделать, что &t[0] будет возвращать не адрес прокси-объекта, а адрес символа в строке. А так как можно делать разные перегрузки для const/не-const, то в случае константных строк адрес может указывать в общий буфер. А в случае модификации *&s[0] = 'a' перегрузка сначала склонировует строку и вернёт адрес, в который можно безопасно писать без риска испортить общую память. Классический copy-on-write.
Вот пример реальной реализации такого proxy-объекта для cow-строк: catboost/string.h.
Re[11]: COW устарел, осторожнее с его использованием?
>> Кто такой Object object? W>Это не важно. Сниппет демонстрирует, что в С++ можно перегружать унарный operator&.
V>>Не вполне понял, что за адрес будет возвращен? W>С помощью перегрузки my_char_proxy::operator& ты можешь сделать, что &t[0] будет возвращать не адрес прокси-объекта, а адрес символа в строке. А так как можно делать разные перегрузки для const/не-const, то в случае константных строк адрес может указывать в общий буфер. А в случае модификации *&s[0] = 'a' перегрузка сначала склонировует строку и вернёт адрес, в который можно безопасно писать без риска испортить общую память. Классический copy-on-write.
W>Вот пример реальной реализации такого proxy-объекта для cow-строк: catboost/string.h.
Ну да, согласен. Хотя не будет правильно работать addressof(str[i]), но это уже придирки..
Re[7]: COW устарел, осторожнее с его использованием?
Здравствуйте, pax123, Вы писали:
P>Здравствуйте, vopl, Вы писали:
P>>>А нельзя для неконстантного operator[] возвращать прокси объект, который имеет оператор преобразования и оператор присваивания, и только в последнем производить расщепление строки?
V>>Нельзя, это явный слом существующего API, что не допустимо.
P>А что именно сломается?
Еще такое addressof(str[i]) — выдает не тот тип и не тот адрес decltype(str[i]) — выдает не тот тип. Чревато это будет в тех местах где результат operator[] подается в перегрузки, в шаблонные штуки всякие и прочие места для которых важен тип именно итогового значения а не его обертки