COW устарел, осторожнее с его использованием?
От: pax123  
Дата: 23.08.22 18:24
Оценка:
Привет!

Наткнулся на статью — C++ велосипедостроение для профессионалов / Хабр.
Там пишут следующее:

Начиная с C++11 COW строчки запрещены в стандарте. Там наложены специальные условия на строчку, что COW реализовать невозможно. Все современные имплементации стандартных библиотек не имеют COW строк.

COW устарел, осторожнее с его использованием.
Аккуратнее используйте COW в новых проектах. Не доверяйте статьям, которым больше 10 лет, перепроверяйте их. COW не показывает таких хороших результатов, как 20 лет назад.


В чем проблема с COW?


Там еще перед этим есть string: COW MT fixes

Например, есть два потока, в них по строке, которые ссылаются на общий динамический объект. Если потоки работают со строками и одновременно решают их удалить, получается, что мы из двух потоков будем пытаться одновременно менять динамический счетчик ссылок use_count. Это приведет к тому, что либо возникнет утечка памяти, либо приложение аварийно завершится.


Не совсем понял, а когда это строки стали thread safe?
Re: COW устарел, осторожнее с его использованием?
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 23.08.22 18:28
Оценка:
Здравствуйте, pax123, Вы писали:

P>Не совсем понял, а когда это строки стали thread safe?


Ну вот представь себе что ты честно вызвал копирование строки, чтобы другая нитка имела свою копию.
А оно взяло и COW применило, а ты и предположить такого не мог.
Понимаешь?
The God is real, unless declared integer.
Re[2]: COW устарел, осторожнее с его использованием?
От: pax123  
Дата: 23.08.22 18:36
Оценка:
Здравствуйте, netch80, Вы писали:

P>>Не совсем понял, а когда это строки стали thread safe?


N>Ну вот представь себе что ты честно вызвал копирование строки, чтобы другая нитка имела свою копию.

N>А оно взяло и COW применило, а ты и предположить такого не мог.
N>Понимаешь?

Да, дошло
Re: COW устарел, осторожнее с его использованием?
От: SaZ  
Дата: 23.08.22 20:47
Оценка:
Здравствуйте, pax123, Вы писали:

P>Привет!


P>Наткнулся на статью — C++ велосипедостроение для профессионалов / Хабр.

P>Там пишут следующее:
P>...
P>В чем проблема с COW?

Проблема на большом количестве потоков / ядер. Каждое обращение к строке — дёргает атомарную операцию, которая в свою очередь вызывает сброс кэша. На 8 ядрах может и не будет заметно, но на 128 уже да. Где-то видел бенчмарки на Qt, где практически все контейнеры реализованы через COW и там просадка очень чувствовалась.

P>Там еще перед этим есть string: COW MT fixes

P>

P>Например, есть два потока, в них по строке, которые ссылаются на общий динамический объект. Если потоки работают со строками и одновременно решают их удалить, получается, что мы из двух потоков будем пытаться одновременно менять динамический счетчик ссылок use_count. Это приведет к тому, что либо возникнет утечка памяти, либо приложение аварийно завершится.


P>Не совсем понял, а когда это строки стали thread safe?


Я так понял, что тут имелась в виду самописная реализация. Несколько нетривиально сделать корректный неблокирующий контейнер. Не будешь же с каждой строкой таскать мьютекс.
Re: COW устарел, осторожнее с его использованием?
От: Андрей Тарасевич Беларусь  
Дата: 23.08.22 20:52
Оценка: +3
Здравствуйте, pax123, Вы писали:

P>В чем проблема с COW?


Проблема с COW не имеет никакого отношения к многопоточности. Проблема с COW заключается в том, что данные в `std::string` официально являются непрерывным массивом и спецификация `std::string` разрешает бесконтрольную раздачу итераторов/указателей/ссылок на этот массив. Это запросто может приводить к возникновению "висящих" итераторов/указателей/ссылок в таком многообразии однопоточных контекстов, что пытаться специфицировать правила инвалидации таких итераторов/указателей/ссылок — бесперспективное занятие.
Best regards,
Андрей Тарасевич
Re[2]: COW устарел, осторожнее с его использованием?
От: pax123  
Дата: 23.08.22 21:03
Оценка:
Здравствуйте, 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 устарел, осторожнее с его использованием?
От: pax123  
Дата: 23.08.22 21:05
Оценка:
Здравствуйте, Андрей Тарасевич, Вы писали:

P>>В чем проблема с COW?


АТ>Проблема с COW не имеет никакого отношения к многопоточности. Проблема с COW заключается в том, что данные в `std::string` официально являются непрерывным массивом и спецификация `std::string` разрешает бесконтрольную раздачу итераторов/указателей/ссылок на этот массив. Это запросто может приводить к возникновению "висящих" итераторов/указателей/ссылок в таком многообразии однопоточных контекстов, что пытаться специфицировать правила инвалидации таких итераторов/указателей/ссылок — бесперспективное занятие.


Не понятно, чем в этом случае строка с COW отличается от строки без этого
Re[3]: COW устарел, осторожнее с его использованием?
От: Videoman Россия https://hts.tv/
Дата: 23.08.22 22:37
Оценка:
Здравствуйте, pax123, Вы писали:

P>Не понятно, чем в этом случае строка с COW отличается от строки без этого


Стандарт предъявляет хитрые требования к итераторам после вызова определенных функций строки. Проще говоря, итератор гарантированно не должен меняться. В случае расщепления COW буфера, когда та или иная "голова" хочет менять данные, указатель на буфер вынужденно меняется, что противоречит стандарту.
Отредактировано 23.08.2022 22:37 Videoman . Предыдущая версия .
Re: COW устарел, осторожнее с его использованием?
От: watchmaker  
Дата: 23.08.22 23:14
Оценка: 20 (3) +4
Здравствуйте, pax123, Вы писали:


P>В чем проблема с COW?


Ну написано же:
1. COW-строки не удовлетворяют новым требованиям, добавленными в C++11
В С++03 явно было сказано, что такой код некорректный и, к примеру, может ронять программу:
{
  std::string s1("s");
  const char* p1s1 = s1.data();
  {
      std::string s2(s);
      const char* p2s1 = s1.data();
      printf("%c", *p2s1);
  }
  printf("%c", *p1s1);
}

В С++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.
Отредактировано 23.08.2022 23:44 watchmaker . Предыдущая версия .
Re[3]: COW устарел, осторожнее с его использованием?
От: SaZ  
Дата: 23.08.22 23:32
Оценка:
Здравствуйте, pax123, Вы писали:

P>Да, я понял. Проблема не столько в COW как таковом, а в его применении для стандартных контейнеров. А так — вполне можно сделать COW контейнер, просто надо указать, что между потоками нужно глубокое копирование/клонирование


Чтобы это сделать, надо для каждой строки будет хранить thread id и запрашивать его на каждый чих. Тоже слишком оверхед. На самом деле, если нужны такие жёсткие оптимизации, то всегда можно сделать узкоспециализированными строками.


P>>>Не совсем понял, а когда это строки стали thread safe?


Я так понял, что тут имелась в виду самописная реализация. Несколько нетривиально сделать корректный неблокирующий контейнер. Не будешь же с каждой строкой таскать мьютекс.

P>Стандартные контейнеры все вроде не тред сейф


Смотря что понимать под "стандартными" и "тред сейф". В Qt это QSharedData и релевантные. Сам COW — всегда потокобезопасный, работа с элементами — нет.
Отредактировано 23.08.2022 23:34 SaZ . Предыдущая версия .
Re[3]: COW устарел, осторожнее с его использованием?
От: Андрей Тарасевич Беларусь  
Дата: 24.08.22 01:31
Оценка: 41 (5) +1
Здравствуйте, 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.

Вот это "except..." и поставило крест на COW.
Best regards,
Андрей Тарасевич
Отредактировано 24.08.2022 4:49 Андрей Тарасевич . Предыдущая версия .
Re: COW устарел, осторожнее с его использованием?
От: Nuzhny Россия https://github.com/Nuzhny007
Дата: 24.08.22 05:33
Оценка: 2 (1) +1
Здравствуйте, pax123, Вы писали:

P>В чем проблема с COW?


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

В стандарт добавили string_view, что решило ещё одну небольшую проблему с производительностью.

Для возвращаемых строк срабатывает же copy ellision? Ещё один плюс к производительности.

Получается, что COW реально может быть полезно лишь в очень узком числе кейсов, для которых можно взять и нестандартные строки.
Re: COW устарел, осторожнее с его использованием?
От: Maniacal Россия  
Дата: 24.08.22 08:16
Оценка:
Здравствуйте, pax123, Вы писали:

P>

P>Начиная с C++11 COW строчки запрещены в стандарте. Там наложены специальные условия на строчку, что COW реализовать невозможно. Все современные имплементации стандартных библиотек не имеют COW строк.
P>COW устарел, осторожнее с его использованием.


P>В чем проблема с COW?


Кстати, в Qt почти все стандартные структуры данных, как я понял, COW. При присвоении присваивается только указатель на уже имеющиеся данные. До первой операции модификации. Уже только тогда экземпляр создаёт себе личную копию данных. Я на эти грабли наступил один раз, когда очень хитрое расширение для массивов ваял, когда в чужих классах можно структуры, хранящиеся в массиве, расширить дополнительными полями, не увеличив размер структуры и не перекомпилируя чужой класс. Тогда пришлось глубже копнуть и накопалась полезная недокументированная функция detach(), которая как раз и вызывается внутри Qt для создания собственной копии данных.
Re[3]: COW устарел, осторожнее с его использованием?
От: sergii.p  
Дата: 25.08.22 08:20
Оценка:
Здравствуйте, pax123, Вы писали:

P>Здравствуйте, netch80, Вы писали:


P>>>Не совсем понял, а когда это строки стали thread safe?


N>>Ну вот представь себе что ты честно вызвал копирование строки, чтобы другая нитка имела свою копию.

N>>А оно взяло и COW применило, а ты и предположить такого не мог.
N>>Понимаешь?

P>Да, дошло


а до меня нет. Ну имеют они ссылку на одно и тоже — в чём проблема? Для программиста это будет выглядеть как копия.
Re[4]: COW устарел, осторожнее с его использованием?
От: Videoman Россия https://hts.tv/
Дата: 25.08.22 09:12
Оценка:
Здравствуйте, sergii.p, Вы писали:

SP>а до меня нет. Ну имеют они ссылку на одно и тоже — в чём проблема? Для программиста это будет выглядеть как копия.


Проблема не в многопоточности, а в новых требованиях стандарта к интерфейсу строки.
Re[5]: COW устарел, осторожнее с его использованием?
От: sergii.p  
Дата: 25.08.22 09:40
Оценка:
Здравствуйте, Videoman, Вы писали:

V>Проблема не в многопоточности, а в новых требованиях стандарта к интерфейсу строки.


это уже ниже сказали. Я увидел, понял, согласен. Думал может ещё есть один довод.
Re[4]: COW устарел, осторожнее с его использованием?
От: pax123  
Дата: 26.08.22 04:37
Оценка:
Здравствуйте, Андрей Тарасевич, Вы писали:

АТ>Один из заезженных примеров, которые приводят в таком случае — это вызов оператора `[]`. Стандарт хочет, чтобы вызов `[]` даже для неконстантного объекта, сам по себе не приводил к расщеплению, т.е. к COPY. В частности, вот в таком примере


АТ>
АТ>std::string a = "abc";
АТ>const char *ptr = a.data();

АТ>{
АТ>  std::string b = a;
АТ>  a[0];
АТ>}

АТ>// ...
АТ>


АТ>для полноценной работы 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 устарел, осторожнее с его использованием?
От: pax123  
Дата: 26.08.22 04:44
Оценка:
Здравствуйте, Nuzhny, Вы писали:

P>>В чем проблема с COW?


N>Современные строки с маленькой длиной ещё в принципе не используют динамическую память. Вроде как, таких кейсов много (адреса, логины, пароли, номера телефонов, тэги и т.д.). Поэтому проблема с производительностью, для которой нужен COW, частично решилась сама собой.


А увеличившийся довольно сильно размер объекта строки не сильно просаживает производительность?


N>В стандарт добавили string_view, что решило ещё одну небольшую проблему с производительностью.


N>Для возвращаемых строк срабатывает же copy ellision? Ещё один плюс к производительности.


N>Получается, что COW реально может быть полезно лишь в очень узком числе кейсов, для которых можно взять и нестандартные строки.


Ясно, спс
Re[4]: COW устарел, осторожнее с его использованием?
От: pax123  
Дата: 26.08.22 04:49
Оценка:
Здравствуйте, sergii.p, Вы писали:

P>>Да, дошло


SP>а до меня нет. Ну имеют они ссылку на одно и тоже — в чём проблема? Для программиста это будет выглядеть как копия.


Работа с общими данными должна быть потокобезопасна. У разных потоков есть своя "копия" строки, соответственно, они не занимаются синхронизацией. И вот они все захотят изменить строку. И тут начнутся проблемы
Re[5]: COW устарел, осторожнее с его использованием?
От: sergii.p  
Дата: 26.08.22 08:17
Оценка:
Здравствуйте, pax123, Вы писали:

P>Работа с общими данными должна быть потокобезопасна. У разных потоков есть своя "копия" строки, соответственно, они не занимаются синхронизацией. И вот они все захотят изменить строку. И тут начнутся проблемы


идню вроде понял. Только наверное вы имели ввиду: "Работа с разделёнными данными должна быть потокобезопасна"
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.