Обновить
20
21
Александр Орефков@orefkov

Программист

Отправить сообщение

Добавил в замеры.
Ожидаемо, получилось не быстро. Хотя влияющих факторов много - получаемый размер (влезет в SSO или нет), сложность самой форматной строки и т.д.

К следующему релизу попробую.

Такой подход вполне хорош, если нам не надо куда-то сохранять результат в другую строку, а нужно просто временно создать строку, которую надо как константную передать какой-то функции, и потом она не нужна.
Либо строку надо формировать как-то сложнее, чем просто конкатенация.
А так у вас получается, сначала в одном буфере собираете строку, а потом перекладываете её в результат, а это снова аллокация и копирование символов. А строковые выражения позволяют сразу собирать строку в буфере результата. Впрочем, и для подобного подхода в библиотеке есть решения, для быстрейшей временной аллокации. Допустим, нам надо найти в тексте "prefx" + искомая строка + "suffix":

size_t pos = src.find(lstringa<255>{"prefx" + search_string  + "suffix"});

lstringa<256> - прямо на стеке выделяется буфер в 256 байт, в нем формируется строка для поиска (если получится длиннее, чем 255, выделит память динамически), после поиска если уложились в буфер - никаких delete. То есть почти всегда ни аллокаций, ни деалокаций.

"Можно, а зачем?" (С)
При желании конечно можно это сделать, но я такой цели себе не ставил - подгонять под чьи то придуманные абстракции. Рэнжи (диапазоны) - отличный механизм для определённых задач, (и работает кстати тоже на expression templates), но зачем уподобляться человеку с молотком в руке, которому всё вокруг начинает казаться гвоздями?
Цель была именно работа со строками и только со строками, без лишних абстракций.
И ключевой момент в том, что объект-строковое выражение позволяет делать по себе ДВА прохода - один раз для подсчёта длины результата, второй раз - для размещения результата. В рэнжах же проход делается один раз, то есть к нему надо прикручивать какой-либо бекинсертер, который будет последовательно добавлять символы в результат, а это именно та проблема, которую я и решаю. Потому что бэкинсертер - это абстракция, что у нас есть "резиновая" строка, к которой можно невозбранно добавлять символы в конец, но как все абстракции, она "протекает". И на физическом уровне резиновой строки нет, под слоем абстракции приходится выделять новую память и перекладывать символы.

Но в целом да, сделать совместимость с range вполне возможно, если стоит задача выводить не в строку, а куда-то в другое место, более приспособленное к последовательному выводу.

Так в том и есть основная сложность в работе со строками, что там все эти вещи влияют и взаимоувязаны. И на практике надо балансировать всеми составляющими - уменьшать количество реаллокаций, уменьшать количество перекладываний из буфера в буфер, аллоцировать быстрее, копировать быстрее, преобразовывать в строку быстрее. Рассмотренный в статье подход из библиотеки simstr - решает две задачи из этого списка - уменьшить количество реалокаций, уменьшить количество перекладываний символов. За счёт того, что сначала считается общая длина строки, и нужный буфер гарантировано выделяется только один раз и все символы гарантированно сразу попадают на своё место и не требуется их перекопирования в другое место.
Но пользователю ничего не мешает добавить к этому при желании более быстрый аллокатор или форматер.
Есть в библиотеке и способ для быстрейшей временной аллокации. Допустим, нам результирующая строка нужна только здесь и сейчас, и сохранять её не нужно, а достаточно только куда-то передать в вызов функции, причём там она гарантированно не меняется, и достаточно константной строки. При этом мы примерно знаем размер результата. Используем lstring подходящего размера. Допустим, нам надо найти в тексте "prefx" + искомая строка + "suffix":

size_t pos = src.find(lstringa<255>{"prefx" + search_string  + "suffix"});

lstringa<256> - прямо на стеке выделяется буфер в 256 байт, в нем формируется строка для поиска (если получится длиннее, чем 255, выделит память динамически), после поиска если уложились в буфер - никаких delete. То есть почти всегда ни аллокаций, ни деалокаций.

Если заменить в методе place на какой-либо insert iterator...

Преусмотрено 4 способа - документация, примеры

Замерил я работу fmt::format, в обоих вариантах. Чтобы было интересней, делал не просто конкатенацию строк и числа, а вот такой код:

for (unsigned i = 1; i <= 100'000; i *= 10) {
    std::string str = fmt::format(FMT_COMPILE("art {:#010o} end"), i);
    benchmark::DoNotOptimize(str);
}

То есть строка плюс число в восьмеричной форме с префиксом 0, дополненное нулями, общей шириной 10, плюс ещё строка. То есть замеряем время построения таких строк:

art 0000000001 end
art 0000000012 end
art 0000000144 end
art 0000001750 end
art 0000023420 end
art 0000303240 end

В simstr есть упрощённые аналоги std::format и std::vformat, но они не поддерживают флагов форматирования внутри форматной строки, аргументами для них должны быть уже готовые строковые выражения в нужном формате, но с такой задачей они справятся.

Результаты на моём Xeon E5-2682v4 2.5GHz получились такие:

                            Benchmark               |  Win10| Ub22  | Ub22  |
                                                    |Clang19|Clang21|CGG-13 |
-----------------------------------------------------------------------------
"art "_ss + i / 0x8a010_fmt + " end"                | 103 ns| 119 ns|92.6 ns|
e_subst("art {} end", i / 0x8a010_fmt)              | 156 ns| 178 ns| 139 ns|
e_vsubst(pattern, i / 0x8a010_fmt)                  | 304 ns| 363 ns| 326 ns|
fmt::format(FMT_COMPILE("art {:#010o} end"), i)     | 850 ns| 512 ns| 489 ns|
fmt::format("art {:#010o} end", i)                  |1233 ns| 738 ns| 733 ns|
std::format("art {:#010o} end", i)                  |1400 ns| 986 ns|1069 ns|
std::vformat(pattern, std::make_format_args(i))     |1381 ns|1011 ns| 873 ns|
strm << "art 0" << std::oct << std::setw(9) << std:)|7312 ns|2052 ns|2011 ns|

а это на godbolt

Да, работает быстрее, чем просто format, но...

Ладно, ладно... Я немного схитрил в этом замере. Здесь замеры для моих способов проводились при конкатенации в мой строковый тип stringa. А в нём размер буфера для SSO не 15 байт, как в std::string, а 23. Поэтому у него результат не требовал аллокаций :) Вот замеры, если во всех тестах конкатенировать в std::string (сразу видим, какой в Win тормозной дефолтный аллокатор):

                            Benchmark               |  Win10| Ub22  | Ub22  |
                                                    |Clang19|Clang21|CGG-13 |
-----------------------------------------------------------------------------
"art "_ss + i / 0x8a010_fmt + " end"                | 599 ns| 203 ns| 207 ns|
e_subst("art {} end", i / 0x8a010_fmt)              | 737 ns| 280 ns| 272 ns|
e_vsubst(pattern, i / 0x8a010_fmt)                  | 896 ns| 467 ns| 545 ns|
fmt::format(FMT_COMPILE("art {:#010o} end"), i)     | 840 ns| 488 ns| 527 ns|
fmt::format("art {:#010o} end", i)                  |1235 ns| 718 ns| 739 ns|
std::format("art {:#010o} end", i)                  |1359 ns| 938 ns|1068 ns|
std::vformat(pattern, std::make_format_args(i))     |1360 ns| 962 ns| 910 ns|
strm << "art 0" << std::oct << std::setw(9) << std:)|6565 ns|2017 ns|1991 ns|

И всё-равно в результате

My string fu is stronger than yours

Хотя, если уж начинать использовать simstr, зачем отказываться от stringa? Она на 8 байт меньше, чем std::string, а буфер SSO на 8 байт больше.

Совершенно верно. Я тут даже статью размещал.
Вообще, Expression Templates были ещё аж в 1994 году придуманы. И даже ещё на том C++ уже могли работать, до всяких концептов и прочего. Только чуть многословнее получались.
На счёт времени жизни - вы тоже совершенно правы, я в статье как-раз сделал оговорку, что

Второе - то, что в C++ промежуточные временные объекты во время выполнения выражения живут до его конца, до точки с запятой

Где-то в документации у меня даже обращается внимание, что "передавать строковое выражение в функцию безопасно, а вот возвращать - нет". Но если очень хочется, то в simstr есть таки способ.

C обычным fmt::format я для себя сравнивал, там результаты примерно одинаковые с std::format.
Спасибо за наводку с FMT_COMPILE - обязательно попробую и дополню - и бенчмарки, и статью, и комментарий. Будет интересно сравнить со своим e_subst, который примерно то же самое умеет делать.

Ничего и не забыли, что вы.
Результаты у snprintf, мягко говоря, не блещут, ещё и с буферами возиться.
Хотя, для любителей, в своём классе lstring я возможность форматировать строку через snprintf сделал.
Естественно, с автоматическим увеличением размера строки при необходимости.

Ну, описываемый мной метод по сути и строит в compile-time древовидную структуру из кусочков строк, и при выполнении схлопывает их в результат. Только здесь эти "кусочки" могут быть не только существующими строками, но и "генераторами", могущими создавать строки на лету. В библиотеке у меня довольно приличное количество разных генераторов, на разные случаи.

Имхо, этот "новый переключатель" работает только пока КПД близкое к нулевому. Когда добьются приемлемого КПД, окажется, что "переключатель" может работать только в одном режиме, а конструкция станет похожа на гидротрансформатор "до степени смешения". И "смешение" здесь от слова "смех".

Если вы не знакомы с корутинами, то перед прочтением "как оно там устроено под капотом" всё-таки стоит познакомиться с ними.

Тут моя недоработка, постараюсь найти время и дописать к статье небольшое вступление.

Там фишка в consexpr. Для каждого типа параметра генерируется только одна ветка

return "....";

Все остальные ветки выкидываются. Создание и возврат 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 отдельно, результат отдельно. Именно это и должен показать этот пример, не то, как бы вам хотелось, а то, как оно есть.

1
23 ...

Информация

В рейтинге
371-й
Откуда
Киров (Кировская обл.), Кировская обл., Россия
Дата рождения
Зарегистрирован
Активность

Специализация

Десктоп разработчик, Бэкенд разработчик
C++
Qt
C++ stl
Разработка программного обеспечения
Многопоточность
Системное программирование
Linux
Git
SQL