Информация
- В рейтинге
- 371-й
- Откуда
- Киров (Кировская обл.), Кировская обл., Россия
- Дата рождения
- Зарегистрирован
- Активность
Специализация
Десктоп разработчик, Бэкенд разработчик
C++
Qt
C++ stl
Разработка программного обеспечения
Многопоточность
Системное программирование
Linux
Git
SQL
Добавил в замеры.
Ожидаемо, получилось не быстро. Хотя влияющих факторов много - получаемый размер (влезет в SSO или нет), сложность самой форматной строки и т.д.
К следующему релизу попробую.
Такой подход вполне хорош, если нам не надо куда-то сохранять результат в другую строку, а нужно просто временно создать строку, которую надо как константную передать какой-то функции, и потом она не нужна.
Либо строку надо формировать как-то сложнее, чем просто конкатенация.
А так у вас получается, сначала в одном буфере собираете строку, а потом перекладываете её в результат, а это снова аллокация и копирование символов. А строковые выражения позволяют сразу собирать строку в буфере результата. Впрочем, и для подобного подхода в библиотеке есть решения, для быстрейшей временной аллокации. Допустим, нам надо найти в тексте "prefx" + искомая строка + "suffix":
lstringa<256> - прямо на стеке выделяется буфер в 256 байт, в нем формируется строка для поиска (если получится длиннее, чем 255, выделит память динамически), после поиска если уложились в буфер - никаких delete. То есть почти всегда ни аллокаций, ни деалокаций.
"Можно, а зачем?" (С)
При желании конечно можно это сделать, но я такой цели себе не ставил - подгонять под чьи то придуманные абстракции. Рэнжи (диапазоны) - отличный механизм для определённых задач, (и работает кстати тоже на expression templates), но зачем уподобляться человеку с молотком в руке, которому всё вокруг начинает казаться гвоздями?
Цель была именно работа со строками и только со строками, без лишних абстракций.
И ключевой момент в том, что объект-строковое выражение позволяет делать по себе ДВА прохода - один раз для подсчёта длины результата, второй раз - для размещения результата. В рэнжах же проход делается один раз, то есть к нему надо прикручивать какой-либо бекинсертер, который будет последовательно добавлять символы в результат, а это именно та проблема, которую я и решаю. Потому что бэкинсертер - это абстракция, что у нас есть "резиновая" строка, к которой можно невозбранно добавлять символы в конец, но как все абстракции, она "протекает". И на физическом уровне резиновой строки нет, под слоем абстракции приходится выделять новую память и перекладывать символы.
Но в целом да, сделать совместимость с range вполне возможно, если стоит задача выводить не в строку, а куда-то в другое место, более приспособленное к последовательному выводу.
Так в том и есть основная сложность в работе со строками, что там все эти вещи влияют и взаимоувязаны. И на практике надо балансировать всеми составляющими - уменьшать количество реаллокаций, уменьшать количество перекладываний из буфера в буфер, аллоцировать быстрее, копировать быстрее, преобразовывать в строку быстрее. Рассмотренный в статье подход из библиотеки simstr - решает две задачи из этого списка - уменьшить количество реалокаций, уменьшить количество перекладываний символов. За счёт того, что сначала считается общая длина строки, и нужный буфер гарантировано выделяется только один раз и все символы гарантированно сразу попадают на своё место и не требуется их перекопирования в другое место.
Но пользователю ничего не мешает добавить к этому при желании более быстрый аллокатор или форматер.
Есть в библиотеке и способ для быстрейшей временной аллокации. Допустим, нам результирующая строка нужна только здесь и сейчас, и сохранять её не нужно, а достаточно только куда-то передать в вызов функции, причём там она гарантированно не меняется, и достаточно константной строки. При этом мы примерно знаем размер результата. Используем lstring подходящего размера. Допустим, нам надо найти в тексте "prefx" + искомая строка + "suffix":
lstringa<256>- прямо на стеке выделяется буфер в 256 байт, в нем формируется строка для поиска (если получится длиннее, чем 255, выделит память динамически), после поиска если уложились в буфер - никаких delete. То есть почти всегда ни аллокаций, ни деалокаций.Если заменить в методе place на какой-либо insert iterator...
Преусмотрено 4 способа - документация, примеры
Замерил я работу
fmt::format, в обоих вариантах. Чтобы было интересней, делал не просто конкатенацию строк и числа, а вот такой код:То есть строка плюс число в восьмеричной форме с префиксом 0, дополненное нулями, общей шириной 10, плюс ещё строка. То есть замеряем время построения таких строк:
В simstr есть упрощённые аналоги
std::formatиstd::vformat, но они не поддерживают флагов форматирования внутри форматной строки, аргументами для них должны быть уже готовые строковые выражения в нужном формате, но с такой задачей они справятся.Результаты на моём Xeon E5-2682v4 2.5GHz получились такие:
а это на godbolt
Да, работает быстрее, чем просто format, но...
Ладно, ладно... Я немного схитрил в этом замере. Здесь замеры для моих способов проводились при конкатенации в мой строковый тип
stringa. А в нём размер буфера для SSO не 15 байт, как вstd::string, а 23. Поэтому у него результат не требовал аллокаций :) Вот замеры, если во всех тестах конкатенировать вstd::string(сразу видим, какой в Win тормозной дефолтный аллокатор):И всё-равно в результате
Хотя, если уж начинать использовать simstr, зачем отказываться от
stringa? Она на 8 байт меньше, чемstd::string, а буфер SSO на 8 байт больше.Совершенно верно. Я тут даже статью размещал.
Вообще, Expression Templates были ещё аж в 1994 году придуманы. И даже ещё на том C++ уже могли работать, до всяких концептов и прочего. Только чуть многословнее получались.
На счёт времени жизни - вы тоже совершенно правы, я в статье как-раз сделал оговорку, что
Где-то в документации у меня даже обращается внимание, что "передавать строковое выражение в функцию безопасно, а вот возвращать - нет". Но если очень хочется, то в simstr есть таки способ.
C обычным
fmt::formatя для себя сравнивал, там результаты примерно одинаковые сstd::format.Спасибо за наводку с
FMT_COMPILE- обязательно попробую и дополню - и бенчмарки, и статью, и комментарий. Будет интересно сравнить со своимe_subst, который примерно то же самое умеет делать.Ничего и не забыли, что вы.
Результаты у
snprintf, мягко говоря, не блещут, ещё и с буферами возиться.Хотя, для любителей, в своём классе
lstringя возможность форматировать строку черезsnprintfсделал.Естественно, с автоматическим увеличением размера строки при необходимости.
Ну, описываемый мной метод по сути и строит в compile-time древовидную структуру из кусочков строк, и при выполнении схлопывает их в результат. Только здесь эти "кусочки" могут быть не только существующими строками, но и "генераторами", могущими создавать строки на лету. В библиотеке у меня довольно приличное количество разных генераторов, на разные случаи.
Имхо, этот "новый переключатель" работает только пока КПД близкое к нулевому. Когда добьются приемлемого КПД, окажется, что "переключатель" может работать только в одном режиме, а конструкция станет похожа на гидротрансформатор "до степени смешения". И "смешение" здесь от слова "смех".
Тут моя недоработка, постараюсь найти время и дописать к статье небольшое вступление.
Там фишка в consexpr. Для каждого типа параметра генерируется только одна ветка
Все остальные ветки выкидываются. Создание и возврат std::string будет относительно быстрым, так как до 15 символов помещаются во внутренний буфер строки без аллокации.
Возвращать std::string_view было бы быстрее, но он не даёт гарантий null-терминированности, если строку надо будет передавать в C-API. Начиная с C++26 вроде будет std::zstring_view - на null-терминированную строку, это был бы лучший вариант. Ну или использовать мою simstr - там это из коробки :)
В статье про анимированные графики не увидел ни одного анимированного графика, а так хотел. Печально :(
Я вообще-то программирую "в уме". Тут даже электричество не нужно. Или вы процесс набирания слов на клавиатуре считаете программированием?
А, понял. Вы свои примером реализовали заготовку для питоновского
for r in hat:.А цель статьи - показать как внутри C++ реализовывается
def magicians_hat(start, end, step):.Да, теперь точно вижу, что вы не поняли назначение как моего примера. так и вообще, для чего в туториалах даются примеры.
У вас, во-первых, получился не базовый класс для построения самих генераторов, а просто класс для получения значений из генераторов.
Во-вторых - его многословность и детали, не относящиеся к построению стейт-машин никак не ведёт к цели статьи - объяснению внутреннего механизма работы корутин.
В-третьих - раз уж вы решили сделать класс для запуска генераторов "по-современному", то и делайте его правильно - где begin(), end(), где iterator, *iterator, ++iterator?
Да уж, концепты в C++98, сильно :)
Для чего promise() в корутинах, тоже совершенно не так поняли.
Ну и state_ в примере - в стэйт-машинах это не "готов-не готов", а точка перехода внутри функции-генератора, она не может быть ограниченна двумя значениями, в наследниках может быть много точек перехода.
Вы когда ручку игрового автомата дёргаете, выигрыш из ручки выпадает, или из лотка? Я бы мог конечно сделать std::optional<int> pullout_rabbit (), но это совсем не то, как работает механизм корутин в C++. У них как раз ручка resume отдельно, результат отдельно. Именно это и должен показать этот пример, не то, как бы вам хотелось, а то, как оно есть.