Я начал экспериментировать с {fmt} и написал небольшую программу, чтобы посмотреть, как она обрабатывает большие контейнеры. Казалось бы, fmt::print() (который в конечном итоге отправляет выходные данные в stdout) внутренне сначала компонует весь результат в виде строки. Тестовая программа ниже, в которой я форматирую vector<char> размером 10 000 000, используя строку форматирования, которая занимает 100 байт на запись, накапливает полные 100 * 10 000 000 = 1 ГБ ОЗУ, прежде чем начать выгружать результат в stdout. Хотя вы не можете этого сказать по результатам моей тестовой программы, почти все 1,7 секунды, необходимые для форматирования и вывода результата, тратятся на форматирование, а не на вывод. (Если вы не перенаправляете на /dev/null, перед тем, как что-либо начнет выводиться на стандартный вывод, произойдет долгая пауза.) Это нехорошее поведение, если вы пытаетесь создать инструменты конвейерной обработки.
Вопрос 1. Я вижу некоторые ссылки в документации на fmt::format_to(). Можно ли это каким-то образом использовать для начала потоковой передачи и удаления результата до завершения форматирования и тем самым избежать внутренней композиции полного результата?
В2. Продолжая эту линию исследования, вместо передачи контейнера, есть ли способ передать, скажем, два итератора (которые, возможно, указывают на начало и конец очень большого файла) и прокачать эти данные через {fmt} для обработки? (и тем самым избежать необходимости сначала читать весь файл в памяти)?
#include <iostream>
#include <vector>
#include "fmt/format.h"
#include "fmt/ranges.h"
#include "time.h"
using namespace std;
inline long long
clock_monotonic_raw() {
struct timespec ct;
clock_gettime(CLOCK_MONOTONIC_RAW, &ct);
return ct.tv_sec * 1000000000LL + ct.tv_nsec;
}
inline double
dt() {
static long long t0 = 0;
if (t0 == 0) {
t0 = clock_monotonic_raw();
return 0.0;
}
long long t1 = clock_monotonic_raw();
return (t1 - t0) / 1.0e9;
}
int main(int argc, char** argv) {
fprintf(stderr, "%10.6f: ENTRY\n", dt());
vector<char> v;
for (int i = 0; i < 10'000'000; ++i)
v.push_back('A' + i % 26);
string pad(98, ' ');
fprintf(stderr, "%10.6f: INIT\n", dt());
fmt::print(pad + "{}\n", fmt::join(v, "\n" + pad));
fprintf(stderr, "%10.6f: DONE\n", dt());
return 0;
}
matt@dworkin:fmt_test$ g++ -o mem_fmt -O3 -I ../fmt/include/ mem_fmt.cpp ../fmt/libfmt.a
matt@dworkin:fmt_test$ ./mem_fmt > /dev/null
0.000000: ENTRY
0.034582: INIT
1.769687: DONE
[из другого окна во время работы]
matt@dworkin:fmt_test$ ps -aux | egrep 'COMMAND|mem_fmt' | grep -v grep
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
matt 30292 2.8 6.2 1097864 999208 pts/0 S+ 17:40 0:01 ./mem_fmt
Примечание ВСЗ объемом 1,097864 ГБ





Во-первых, давайте рассмотрим ваш пример. Текущая версия {fmt} имеет оптимизацию, позволяющую производить запись непосредственно в буфер потока. На данный момент он включен только для фундаментальных и строковых типов. После включения join_view в этого коммита в вашем примере не будет выделяться дополнительная динамическая память, fmt::print будет просто использовать буфер потока C.
В отличие от подхода ostream_iterator, это также будет быстрее.
До:
% time ./a.out > /dev/null
...
./a.out > /dev/null 0.23s user 0.38s system 71% cpu 0.857 total
После:
% time ./a.out > /dev/null
...
./a.out > /dev/null 0.12s user 0.01s system 96% cpu 0.135 total
Эта оптимизация также предложена (и принята) для std::print в P3107R5.
Разрешить эффективную реализацию std::print .
В старых версиях {fmt} вы можете просто заменить fmt::join написание строк по отдельности, fmt::join в любом случае не принесет никакой пользы в вашем случае.
Теперь к вопросам:
Вопрос 1. Я вижу в документации некоторые ссылки на fmt::format_to(). Можно ли это каким-то образом использовать для начала потоковой передачи и удаления результата до завершения форматирования и тем самым избежать внутренней композиции полного результата?
Да. В общих функциях форматирования, включая format_to, запись в буфер фиксированного размера (print было исключением, но оно исправляется, как описано выше). Им все равно может потребоваться выделить один аргумент (но не весь вывод), если вы используете заполнение.
В2. Продолжая эту линию исследования, вместо передачи контейнера, есть ли способ передать, скажем, два итератора (которые, возможно, указывают на начало и конец очень большого файла) и прокачать эти данные через {fmt} для обработки? (и тем самым избежать необходимости сначала читать весь файл в памяти)?
Да. {fmt} выполняет итерацию по элементу диапазона и поддерживает однопроходные итераторы ввода. Таким образом, вы можете лениво читать и отбрасывать части ввода после того, как они были использованы, чтобы сэкономить память. Итераторы можно передавать как часть диапазона или через fmt::join.
То есть вы говорите, что мне просто нужно скачать последнюю версию? И есть ли у вас какие-либо идеи по моему второму вопросу? format_to позволяет вам установить итератор вывода, но я не заметил интерфейса, позволяющего использовать итератор для ввода. Было бы неплохо передать формат.
На вопросы ответил более прямо. Вчера немного спешил и сосредоточился только на примере. ХТХ
Я обнаружил, что
fmt::format_to(ostream_iterator<char>(std::cout), pad + "{}\n", fmt::join(v, "\n" + pad));решает проблему с памятью за счет 20-кратного снижения производительности. Возможно, можно было бы разработать какой-нибудь более простой итератор, который либо записывает данные непосредственно в стандартный вывод, либо в какой-нибудь буфер размером 4 КБ, который выгружает данные в стандартный вывод частями. Мне все равно хотелось бы знать, сможет ли кто-нибудь найти лучшее решение моего первого вопроса. И найдите любое решение моего второго вопроса.