Pull to refresh

Comments 25

PinnedPinned comments

Замерил я работу 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 байт больше.

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

Везде конкатенируют )

В v8 (js) под капотом используется Rope, и потому "сложение" не такое грустное, как на первый взгляд. Хотя конечно медленнее специализированного варианта.

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

В v8 (js) под капотом используется Rope, и потому "сложение" не такое грустное,

где-то на хабре помнится была статья, где пару простых присвоений строк приводили к циклическим ссылкам и утечке памяти как результат. Так что тут на самом деле как повезёт.

проблема глубже, поидее, на мой не экспертный взгляд тут проблема владения, у большой строки, даже если она строка должно быть выделено фиксированное количество символов,

доставщики должны быть фиксированные поидее, фиксация за кадр должна быть 1 если это критично

это чем-то похоже на воксели, я теперь везде воксели(кстати советую, там можно сделать не на компут шейдерах, будет прям мотоцикл в миниатюре, с буферами и память не перевыделяется) вижу, потомучто тут красивейшая структура управления ресурсами памяти

если это очень большие данные можно замапать 3и буфера - как три страницы(1х-4096), 1 страница - это временный буфер(но удалять его нельзя, только очищать это циркуляция данных), 2 другие рабочие типо, если такие строки с доставщиками правильно отстроить выделение памяти остановится поидее

Программирование не заканчивается на геймдеве, и даже не начинается там.

Если что, эта техника называется "expression templates". Главное здесь - правильно следить за лайфтаймами, а то можно случайно... да... Константные ссылки в полях класса, как известно, не продлевают лайфтайм.

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

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

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

std::format меделнный, потому что в runtime всё делает.
Можете для эксперимента добавить вариант на популярной библиотеке {fmt} с compile-time выражением?

#include <fmt/format.h>
#include <fmt/compile.h>

std::string make_answer_fmt(std::string_view str_answer, int count) {
    return fmt::format(FMT_COMPILE("The answer is {}, count is {}"), str_answer, count);
}

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

Замерил я работу 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 байт больше.

В том то и дело - не понятно что профилируете, толи форматтер, толи конкатенатор, толи вообще аллокатор. Кому нужна скорость, каким-нибудь геймдевам, то за лишнюю аллокацию удавятся, хотя бы format_to(). Если руками не хочется писать преобразование числа в символы, то хотя бы std::to_chars.

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

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

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

Ещё забыли классику - char buf[1024] /* Должно хватить :-) */; snprintf(buf, sizeof(buf), ...; return std::string(buf). Можете добавить в бенчмарк для общей статистики?

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

Невыдуманные истории о которых невозможно молчать

а я правильно понял, что генератор тут это i / 0x8a010_fmt ? А свои генераторы можно/легко писать? Если мне надо сконкатенировать со строкой например вектор моих типов каких-то?

Ещё можно развить идею и не конкатенировать строковое выражение в одну строку а, например, записывать выражение в файл, или дать возможность итерации по такой "строке".

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

Я глубоко в ваш код не копал, но выглядит так, что можно из строкового выражения сделать диапозон с input_iterator (генерировать значения на лету для случая преобразователей чисел в строки или ленивых генераторов). Так получится полная совместимость с C++20 ranges и всеми её плюшками. Ну и то что я озвучил выше, на этой основе можно реализовать.

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

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

А если такой вариант протестировать (с сохранением типов std::string_view str_answer, int count в аргументах):

thread_local std::string buffer;

std::string format(std::string_view str_answer, int count) {
    buffer.clear(); // Очищаем содержимое, но сохраняем выделенную память
    if (buffer.capacity() < 1024) { 
        buffer.reserve(1024); // Минимальный размер (упрощенно, не в конструкторе)
    }

    buffer.append("The answer is ").append(str_answer).append(", count is ");
    std::format_to(std::back_inserter(buffer), "{}", count); // C++20: Пишем число в буфер без аллокаций

    return buffer;
}

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

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

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

Ну так и в перфомансе – вы сами решаете, где нужна вам аллокация и копирование, – а где только временный объект. Но, никто не мешает создать чанки на стеке через alloca и спец объект (класс), но по принципу "Парето" простого решения достаточно в 80% случаев.
Например, если для целей логирования, то аллокации на стеке совсем будут не нужны. А для вставки, например, в map вы ее сделаете сами, возможно сразу в placement new выделенный буфер для ключей (чтобы сохранить локальность данных).

Sign up to leave a comment.

Articles