Как использовать {fmt} с большими данными

Я начал экспериментировать с {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::format_to(ostream_iterator<char>(std::cout), pad + "{}\n", fmt::join(v, "\n" + pad)); решает проблему с памятью за счет 20-кратного снижения производительности. Возможно, можно было бы разработать какой-нибудь более простой итератор, который либо записывает данные непосредственно в стандартный вывод, либо в какой-нибудь буфер размером 4 КБ, который выгружает данные в стандартный вывод частями. Мне все равно хотелось бы знать, сможет ли кто-нибудь найти лучшее решение моего первого вопроса. И найдите любое решение моего второго вопроса.

Matthew Busche 09.05.2024 04:15
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
1
294
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Во-первых, давайте рассмотрим ваш пример. Текущая версия {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 позволяет вам установить итератор вывода, но я не заметил интерфейса, позволяющего использовать итератор для ввода. Было бы неплохо передать формат.

Matthew Busche 10.05.2024 13:32

На вопросы ответил более прямо. Вчера немного спешил и сосредоточился только на примере. ХТХ

vitaut 10.05.2024 15:34

Другие вопросы по теме