自研轻量日志组件更适配嵌入式或短生命周期场景,需规避宏参数提前求值、异步线程退出、缓冲区切换、std::ofstream隐式阻塞四大坑。

直接用 spdlog 是最省心的选择,但如果你需要极简可控、无第三方依赖、或嵌入式/短生命周期场景(如命令行工具),自己写一个轻量日志组件反而更稳——关键不是“能不能写”,而是避开几个硬坑:宏展开时的参数求值、异步线程提前退出、缓冲区切换时机、以及 std::ofstream 的隐式阻塞。
为什么不能在日志宏里直接调用 expensive_func()?
常见错误是写成 LOG_INFO("result = {}", expensive_func());,哪怕日志级别被关掉,expensive_func() 仍会执行。这不是 spdlog 的锅,是 C++ 宏不带条件编译能力导致的。
- 正确做法:用编译期过滤宏,把级别判断压到预处理层,例如:
#define LOG_DEBUG(fmt, ...) do { if (LOG_LEVEL >= DEBUG) log_impl(DEBUG, __FILE__, __LINE__, fmt, ##__VA_ARGS__); } while(0) - 所有参数必须延迟求值——
log_impl内部再做格式化,而非宏里直接展开 - 若用
fmt::format_to,目标缓冲必须是栈上预分配(如fmt::memory_buffer),避免每次调用 malloc
异步写入必须显式 flush + shutdown,否则日志大概率丢失
异步模式下,后台线程靠条件变量或 sleep 轮询消费队列,主线程退出时它很可能还没处理完缓冲区。这不是 bug,是设计使然。
- 必须在
main()结尾调用spdlog::shutdown();自研组件则需提供Logger::flush_and_join()并确保调用 - 短生命周期程序(如单元测试、CLI 工具)禁用异步——直接用
spdlog::basic_logger_st或同步模式 - 如果用了
async_logger,初始化时务必设spdlog::drop_policy::overrun_oldest,否则队列满就丢新日志,且不报错
文件写入别碰 std::ofstream,改用 writev + mmap + 预分配
std::ofstream 看似简单,实则隐藏三重开销:流缓冲不可控、异常机制拖慢路径、底层 write() 调用无法批量提交。
- 生产环境文件写入应跳过 iostream,直接用
writev()(Linux)或WriteFile()(Windows)一次提交多条日志 - 日志文件提前
ftruncate()预分配 1GB 空间,再mmap()映射,写内存即写磁盘,最后msync()刷盘 - 滚动策略用原子计数器:记录当前文件已写字节数,达
rollSize_(如 2MB)就原子替换句柄,不锁全局
格式化必须推迟到后台线程,且禁用 locale
格式化开销常被低估:std::put_time 构造 std::locale、snprintf 解析格式串、临时 std::string 分配——全在业务线程发生,就等于卡主线程。
- 时间戳用
clock_gettime(CLOCK_MONOTONIC, &ts)+ 自己解析微秒,比std::chrono快 3–5 倍 - 禁用
fmt的 locale 支持:编译时定义FMT_LOCALE=0,避免隐式构造 - 日志消息结构体固定 256 字节,含 level、ts、tid、len、content[],后台线程统一格式化落盘
真正难的不是写异步线程,而是让“不写日志”和“写日志”两条路径的性能差控制在 10ns 以内——这要求所有判断、拷贝、格式化都得推到编译期或后台线程,连 __FILE__ 这种字符串字面量都得考虑是否进符号表。别信“加个 mutex 就线程安全”,线程安全的代价是 cache line bouncing,而日志系统的第一守则:永远别让 IO 拖慢主逻辑。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











