如何在处理大数据时高效使用{fmt}库?
我开始使用{fmt}库,编写测试程序验证其处理大型容器的表现。测试发现fmt::print()会先在内存中生成完整格式化字符串,再输出到stdout:对含1000万个元素的vector<char>,每个元素格式化占100字节,会先占用1GB内存再输出,1.7秒耗时中几乎全部用于格式化,不利于构建流水线工具。
问题
- 问题1:文档中提及的
fmt::format_to()是否可实现流式输出,避免在内存中生成完整结果? - 问题2:是否支持传入迭代器(如指向大文件首尾的迭代器)处理数据,无需将整个文件读入内存?
测试代码及运行结果
测试代码
#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
注:运行时VSZ为1.097864 GB
问题解答
问题1:关于fmt::format_to()的流式输出能力
fmt::format_to()本身不直接实现流式输出,但可以结合自定义输出迭代器实现边格式化边输出,避免在内存中存储完整结果。
你可以使用fmt::ostream_iterator(需包含<fmt/ostream.h>),它会将格式化后的内容直接写入指定输出流(比如stdout),而非先构建完整字符串。示例用法:
#include <fmt/ostream.h> // ... 其他代码 ... fmt::format_to(fmt::ostream_iterator<char>(std::cout), pad + "{}\n", fmt::join(v, "\n" + pad));
fmt::join处理大型容器时是通过迭代器逐个处理元素的,结合fmt::ostream_iterator后,每个元素格式化完成会立即输出,不会在内存中保留完整格式化结果。
更高效的方式是手动遍历容器元素,逐个格式化输出,完全控制内存使用:
for (char c : v) { fmt::print("{}{}\n", pad, c); }
这种方式每个元素仅分配少量临时内存用于格式化,内存占用大幅降低,输出可流式进行,适配流水线工具需求。
问题2:支持传入迭代器处理数据吗?
{fmt}库支持通过迭代器处理数据,无需将整个文件或容器读入内存。
针对文件场景,可使用标准库std::ifstream结合std::istreambuf_iterator<char>,逐个读取文件字符并格式化输出,无需一次性加载整个文件:
#include <fstream> #include <fmt/format.h> int main() { std::ifstream file("large_file.txt"); std::istreambuf_iterator<char> begin(file), end; string pad(98, ' '); for (auto it = begin; it != end; ++it) { fmt::print("{}{}\n", pad, *it); } return 0; }
这种方式仅在内存中保留当前处理的单个字符和格式化临时内容,内存占用极低,适合超大文件处理。
另外,fmt::join本身接受迭代器范围作为参数,可直接传入文件迭代器范围,结合fmt::ostream_iterator实现流式输出:
fmt::format_to(fmt::ostream_iterator<char>(std::cout), pad + "{}\n", fmt::join(begin, end, "\n" + pad));
{fmt}处理迭代器范围时通常是逐个处理,不会一次性加载全部数据,可放心使用。
内容的提问来源于stack exchange,提问作者Matthew Busche

