fmt
fmt 性能与成本:缓冲区增长、编译格式和可信基准
检查 500 元素内联缓冲区与扩容策略,区分格式检查和 FMT_COMPILE,并分别测量格式化、分配、I/O 与构建成本。
你将学会
- 缓冲区有具体策略,但没有通用零分配承诺
- 检查格式不等于编译格式化器
- 测量应用真正付出的工作
开始前需要
- 理解基础 C++ 值、引用、字符串与构建目标
- 能够区分文档预期与实际测量结果
追踪具体 fmt 行为,在输出、生命周期和验证边界明确的前提下选择集成方式。
先看结论
- 500 元素内联容量属于具体类型和版本。
- 编译格式用专用代码换取优化机会,也可能增加体积。
- 分开测格式化、分配、I/O 和构建,不虚构节省数字。
缓冲区有具体策略,但没有通用零分配承诺
所查版本的 basic_memory_buffer 默认内联存储 500 个元素,因此常用 char 别名起始时拥有 500 字节内联字符空间。扩容先计算 old_capacity + old_capacity / 2,再根据更大的请求和分配器限制调整。这是可以阅读和测试的具体策略,不是泛泛的“自动优化内存”。
下面的简化算术示例刻意忽略分配器上限和溢出,只适用于较小的非负输入。它预测从容量 500 满足请求 501 时扩到 750,而一次请求 900 时直接满足 900。这些数字描述策略,不是吞吐量实测,也不能证明所有 fmt::format 调用都不分配内存,因为转换成 std::string 还可能引入另一笔分配。
检查格式不等于编译格式化器
字面量格式检查会在受支持的 C++20 配置下发现不匹配。FMT_COMPILE 更进一步,可以解析固定格式并生成专门的格式化代码。compile.h 的这条路径要求 C++17 constexpr-if 等能力;不满足条件时,宏会退回 FMT_STRING。因此,实际编译器特性会改变同一个宏调用的行为。
API 指南提醒,格式字符串编译可能增加二进制代码体积,建议只用于已确认的格式化瓶颈。应比较具有代表性的热点调用,而不是机械替换所有格式字符串。运行时间、编译时间和最终二进制大小需要一起报告;孤立循环更快,不一定适合包含大量不同字符串的大型应用。
测量应用真正付出的工作
基准中应把字符串格式化、内存分配和输出 I/O 分成独立阶段。保持输入值、呈现规则、优化选项和工具链一致,并确保结果被使用,避免编译器把工作消除。覆盖短消息、长消息、缓冲区复用和有代表性的自定义类型,而不只挑选一个漂亮的整数例子。
formatted_size 会用计数缓冲区走过格式化路径,format_to_n 也会继续统计未截断输出。因此,先求大小再格式化不一定是免费的两步优化,小写入上限也不能证明超大宽度的 CPU 工作有界。本次复核不提供未经实测的吞吐量、延迟、分配次数或费用节省结论。
实施步骤
- 1
固定版本、编译器、优化模式与代表性输入。
- 2
把格式化时间和输出设备延迟分开测量。
- 3
比较默认方式、缓冲区复用和少量热点编译格式。
- 4
记录二进制与构建影响,把未测量值明确留空。
可复制示例
function teachingCapacity(oldCapacity, requested) {
return Math.max(oldCapacity + Math.floor(oldCapacity / 2), requested);
}
console.log(teachingCapacity(500, 501), teachingCapacity(500, 900));
// 750 900;仅为小输入模型,不是分配器或性能基准常见问题
有内联缓冲区就能证明 fmt::format 从不分配内存吗?
不能。较长结果可能使缓冲区扩容,返回的 std::string 也有自己的存储行为。
应该给所有字面量加 FMT_COMPILE 吗?
不应在缺少测量时这样做。上游说明它可能增加二进制体积,建议针对已经确认的格式化热点使用。
资料来源
- README.md来源核查 2026-09-08
- doc/api.md来源核查 2026-09-08
- include/fmt/core.h来源核查 2026-09-08
- include/fmt/format.h来源核查 2026-09-08
- include/fmt/compile.h来源核查 2026-09-08