Сообщение Re: COW устарел, осторожнее с его использованием? от 23.08.2022 23:14
Изменено 23.08.2022 23:44 watchmaker
Re: COW устарел, осторожнее с его использованием?
Здравствуйте, pax123, Вы писали:
P>В чем проблема с COW?
Ну написано же:
В С++11 требования изменили, и сказали, что такой код падать не должен, даже если из-за этого он будет работать медленнее.
Например, operator[] возвращает ссылку на char. Но он не знает что с ней будут делать: только читать или ещё и записывать. Если записывать, то COW (что переводится как copy-on-write, напомню) вынуждает реализацию склонировать строку. В результате иногда COW-строки работают не так эффективно как могли бы: в них есть ложные копирования из-за того, что программист вызывал неконстантную перегрузку вместо константной.
Но расставлять const или писать везде as_const утомительно...
На самом деле полная фигня и упереться в это в реально программе довольно сложно. Обычно либо строки разные, либо для них достаточно быстро вызывается метод clone/detach, либо они константные и атомики не трогаются.
Упереться разве что можно, если у тебя в реализации пустая строка сделана синглтоном и существует в единственном экземпляре. Тогда все потоки с пустыми строками будут теребить этот несчастный счётчик. Впрочем в практичных реализациях COW-строк этот случай учтён: например, введением строк с глобальным временем жизни, у которых не нужно трогать счётчик, так как строка заведомо живёт дольше остальной программы. И пустая строка как раз попадает в эту категорию.
Примерно такой список причин по убыванию важности.
И да, у не-COW строк тоже свой список недостатков: нельзя сказать, что они в любом сценарии лучше.
P>Там еще перед этим есть string: COW MT fixes
P>
Ты же понимаешь, что это стиль написания статьи такой: сначала дать заведомо неправильную реализацию, а потом показать как этот фрагмент кода исправить?
То что написано в этом абзаце относится только к первой версии реализации строки из статьи, а не к реализациям std::string в популярных версиях STL.
P>Не совсем понял, а когда это строки стали thread safe?
Во-первых, до С++11 в языке не было вообще ничего про многопоточность. Реализации поддерживали многопоточность, все ей пользовались, но в стандарте её не было. Поэтому и не было требования thread-safe у контейнеров.
Во-вторых, у других стандартных контейнеров вполне себе есть подмножество операций, которые обязаны быть потокобезопасными (например, но не только). И со строками аналогично. Так что надо уточнять какие операции ты имеешь ввиду: некоторые потокобезопасные, другие — нет.
В-третьих, в этом фрагменте статьи идёт речь о совсем другой ситуации: не когда два потока меняют одну и ту же переменную строкового типа, а когда каждый поток меняет свою переменную, которая находится у него в эксклюзивном владении:
Тут каждый поток меняет только свою строку: v[0] либо v[1]. Это разные переменные к которым доступ идёт из разных потоков. Поэтому даже если бы было сказано, что ни одна операция с std::string не является thread-safe, то такой код всё равно должен работать правильно.
И в нормальных реализация COW-строк он действительно работает всегда правильно, даже не смотря на то, что все строки ссылаются изначально на один и тот же общий буфер.
Вот как например std::string устроена в libstdc++ до С++11: bits/cow_string.h — вообще никаких проблем с этим сценарием. В других версиях STL для С++03 — аналогично.
И в статье не говорится, что описанная ситуация — это какой-то фатальный сценарий для правильной работы COW-строк, в ней скорее говорится, что если ты реализуешь COW-контейнер вручную, то об этой ситуации просто нужно не забыть.
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-контейнер вручную, то об этой ситуации просто нужно не забыть.
Re: COW устарел, осторожнее с его использованием?
Здравствуйте, pax123, Вы писали:
P>В чем проблема с COW?
Ну написано же:
В С++11 требования изменили, и сказали, что такой код падать не должен, даже если из-за этого он будет работать медленнее.
Например, operator[] возвращает ссылку на char. Но он не знает что с ней будут делать: только читать или ещё и записывать. Если записывать, то COW (что переводится как copy-on-write, напомню) вынуждает реализацию склонировать строку. В результате иногда COW-строки работают не так эффективно как могли бы: в них есть ложные копирования из-за того, что программист вызывал неконстантную перегрузку вместо константной.
Но расставлять const или писать везде as_const утомительно...
На самом деле полная фигня и упереться в это в реально программе довольно сложно. Обычно либо строки разные, либо для них достаточно быстро вызывается метод clone/detach, либо они константные и атомики не трогаются.
Упереться разве что можно, если у тебя в реализации пустая строка сделана синглтоном и существует в единственном экземпляре. Тогда все потоки с пустыми строками будут теребить этот несчастный счётчик. Впрочем в практичных реализациях COW-строк этот случай учтён: например, введением строк с глобальным временем жизни, у которых не нужно трогать счётчик, так как строка заведомо живёт дольше остальной программы. И пустая строка как раз попадает в эту категорию.
Примерно такой список причин по убыванию важности.
И да, у не-COW строк тоже свой список недостатков: нельзя сказать, что они в любом сценарии лучше.
P>Там еще перед этим есть string: COW MT fixes
P>
Ты же понимаешь, что это стиль написания статьи такой: сначала дать заведомо неправильную реализацию, а потом показать как этот фрагмент кода исправить?
То что написано в этом абзаце относится только к первой версии реализации строки из статьи, а не к реализациям std::string в популярных версиях STL.
P>Не совсем понял, а когда это строки стали thread safe?
Во-первых, до С++11 в языке не было вообще ничего про многопоточность. Реализации поддерживали многопоточность, все ей пользовались, но в стандарте её не было. Поэтому и не было требования thread-safe у контейнеров.
Во-вторых, у других стандартных контейнеров вполне себе есть подмножество операций, которые обязаны быть потокобезопасными (например, но не только). И со строками аналогично. Так что надо уточнять какие операции ты имеешь ввиду: некоторые потокобезопасные, другие — нет.
В-третьих, в этом фрагменте статьи идёт речь о совсем другой ситуации: не когда два потока меняют одну и ту же переменную строкового типа, а когда каждый поток меняет свою переменную, которая находится у него в эксклюзивном владении:
Тут каждый поток меняет только свою строку: 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.
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.