I'm starting to play with {fmt} and wrote a little program to see how it processes large containers. It would seem that fmt::print() (which ultimately sends output to stdout) internally first composes the entire result as a string. The test program below where I format a 10,000,000 sized vector<char> using a format string that consumes 100 bytes per entry amasses the full 100 * 10,000,000 = 1 GB of RAM before starting to dump the result to stdout. Although you can't tell from the output of my test program, almost all of the 1.7 seconds it took to format and output the result is spent in the formatting -- not the outputting. (If you don't redirect to /dev/null, there's a long pause before anything starts printing to stdout.) This is not good behavior if you're trying to build pipelining tools.
Q1. I do see some references in the docs to fmt::format_to(). Can that somehow be used to start streaming and discarding the result before the formatting is complete and thereby avoid the in-core composition of the full result?
Q2. Continuing along this line of exploration, instead of passing a container, is there a way I can pass, say, two iterators (that perhaps point at the beginning and ending of a very large file) and pump that data through {fmt} for processing (and thereby avoid having to first read the entire file into memory)?
#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
[from another window whilst it's running]
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
Note VSZ of 1.097864 GB
fmt::format_to(ostream_iterator<char>(std::cout), pad + "{}\n", fmt::join(v, "\n" + pad));solves the memory problem at the cost of a 20x performance hit. Perhaps some simpler iterator that either writes directly to stdout or perhaps to some 4K buffer that dumps to stdout in chunks could be devised. I would still like to know if someone can find a better solution to my first question. And find any solution to my second question.