C++日志框架有非常多,如何进行选择,其中性能是一个非常重要的考量因素,本文对几种有代表性的日志框架进行基准测试,并深入到源码和实现原理进行分析对比,便于大家进行选择。
测试对象
log4cplus属于仿log4j一类,也是起步较早的日志框架,早在2010年,出于log4j的名气,我在项目中就采用了log4cplus;spdlog属于比较现代化的日志框架,专注于性能;boost.log是1.54进入boost库的,当年选择log4cplus时还在评审阶段,boost可以说是准C++标准库,能够进入boost库自然质量不低,但是其设计相对也比较复杂,boost.log使用起来着实不太方便,它不太像直接面向用户使用,而更像是作为底层库供用户封装为更上层库。
日志框架对比
三种日志库在一些特性和实现方式上存在比较大的差别,为更好理解他们,先做一个简单对比:
log4cplus | spdlog | boost.log | |
---|---|---|---|
线程安全 | 默认线程安全 | 分mt和st | 分mt和st |
异步支持 | 从1.1.0开始增加asyncappender,可配置队列深度,线程数不可配,总为1,深度满后阻塞调用。每个日志对象所有。log4cplus并没有提供显式flush的接口,只有shutdown日志对象或close appender间接flush。(log4cplus的同步appender构造函数都有一个immediateFlush参数,表示C++标准输出流或文件流的flush) | 可设置队列深度和线程数,深度满后有阻塞和丢弃老的两种策略;使用全局或每个日志独有都可以。 | 队列有限制深度/不限制深度以及fifo/ordering组合起来四种,限制深度的队列满后有阻塞和丢弃新的两种策略;线程数总为1个;sink frontend所有。 |
日志对象和sink的关系 | sink在log4cplus中称为appender,每个日志对象显式指定appender。 | 每个日志对象显式指定sink。 | 无法显式指定日志对象(boost.log称为log source)对应到哪个sink,换言之每个日志对象对应所有sink,要实现某个日志对象只输出到某个sink,需要通过在sink上设置filter来实现。具体可参考:use-channel-hiearchy-of-boost-log-for-severity-and-sink-filtering,connecting a logger to a specific sink |
日志继承 | 支持,类似log4j,日志继承有一个不好的地方,日志会自动写到父日志,而root日志是所有日志的祖先,如果配置了root日志,总会写到其中,这样会导致写入效率劣化严重 | 不支持 | 不支持 |
配置文件 | 支持,类似log4j | 不支持 | 支持 |
自定义日志级别 | 支持 | 不支持 | 支持 |
boost.log
boost.log相对复杂,单独做一个简单介绍。boost.log使用了很多新的概念,设计上非常模块化,可以灵活组装和扩展。对于初次使用boost.log,上手不会特别顺畅,需要理解清楚以下几个核心概念:
- Log source:就是其他框架中的日志对象,使用者直接面对的,其特别之处是日志级别也是完全自定义的;
- Log sink frontend:异步和同步在这一层实现,sink backend作为参数传给它,创建之后add到core中;
- Log sink backend:代表日志输出的载体,如控制台、文件等,这是我们比较熟悉的概念,把sink分为frontend和backend,我想是为了各层都可以独立扩展,非常灵活;
- Logging core:它是单例的,建立source和sink之间的连接,就像一个hub一样,还可以设置一些全局fiter和属性。
以下列出目前boost.log支持的所有source和sink:
1 | namespace logging = boost::log; |
测试工具&代码
-
benchmark 1.5.0
对benchmark的使用遇到一点小障碍,因为我希望将flush的时间也统计在内,但是benchmark的默认计时方式是无法在最后一个迭代,加入对flush的调用,因为最后一个迭代会自动停止计时。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19for (auto _ : state) {
function_to_be_measured();
}
// 等价于
for (auto it = state.begin(), e = state.end(); it != e; ++it) {
function_to_be_measured();
}
// 我希望这样,但是没有成功,因为!=操作符中,如果到达迭代最后,会自动停止计时
for (auto it = state.begin(), e = state.end();;) {
if (!(it != e)) {
log.flush();
break;
}
log.info(...);
++it;
}因此,最终只有选择UseManualTime计时方式。
-
测试代码:<https://github.com/zhongpan/cpp-logging-benchmark
- 时间和速率统计包含flush时间,也就是保证日志全部输出到控制台或文件;
- 异步写入都是采用1个线程,队列深度都设置为8192,队列满后都采用阻塞策略;
- 写入消息长度最小选择32,避免string的小字符串性能优化,能够代表更通常的场景;
测试结果
-
测试环境:
Intel® Core™ i5-7200U CPU @ 2.50GHz
Micron 1100 SATA 256G -
测试结果:
a_c_32 | a_c_512 | s_c_32 | s_c_512 | |
---|---|---|---|---|
log4cplus | 298.557/s | 47.8782/s | 313.306/s | 50.7682/s |
spdlog | 14.4967k/s | 8.49778k/s | 15.5747k/s | 6.09162k/s |
boost.log | 6.55519k/s | 3.86561k/s | 4.98684k/s | 3.22393k/s |
a_f_32 | a_f_512 | s_f_32 | s_f_512 | |
log4cplus | 15.8821k/s | 5.48616k/s | 11.2635k/s | 4.91378k/s |
spdlog | 180.147k/s | 109.091k/s | 229.206k/s | 135.489k/s |
boost.log | 29.3718k/s | 25.9405k/s | 15.7826k/s | 14.6118k/s |
- 说明:
-
32/512表示消息字节数;
-
a/s表示异步或同步;
-
c/f表示控制台或文件;
结果分析
- 所有的日志框架都表现出文件的写入效率远高于控制台,因此尽量使用文件方式写日志;
- 同步和异步两种方式,如果把flush的时间统计在内,则效率区别不大,但是异步方式不会阻塞应用,如果考虑对应用的效率影响,异步方式显著优于同步,但是可能存在丟日志的情况,在应用退出时必须主动flush;
- 当消息长度增加时,写入效率都有所下降,spdlog当消息大于500字节时下降显著(见下面的源码分析),boost.log下降不明显;
- 各种情况写入效率都是:spdlog>boost.log>log4cplus,并且log4cplus的控制台写入效率非常非常低,spdlog的效率显著高于其他日志框架,特别是文件方式写入效率,让人难以置信👍,boost.log在复杂的设计下仍然保持良好的性能,也非常厉害。
spdlog性能优化源码分析
- std::string_view的使用避免字符串复制时内存分配
1 | // spdlog/details/logger_impl.h |
1 | // spdlog/common.h |
- 实现fmt::memory_buffer进行字符串格式化,500长度以内使用内部数组,避免堆内存分配,并且比sstream更快
1 | // spdlog/sinks/basic_file_sink.h |
1 | // spdlog/fmt/bundled/format.h |
- 缓存时间字符串,避免重复格式化
1 | // spdlog/details/pattern_formatter.h |
- 控制台输出使用CRT接口,相对于ostream更快
1 | // spdlog/details/console_globals.h |
1 | // corecrt_wstdio.h in windows sdk 10.0.17763.0 |
- 文件写入使用的CRT接口,相对于ofstream快
1 | // sdplog/details/file_helper.h |
总结
三种日志框架各有所长,我觉得日志框架最重要的还是性能和接口易用性,为了调试bug,必须打必要的日志,但是开了日志,又影响到程序性能,甚至本来必现的问题,因为效率变差的原因都不出来了,这就尴尬了。经过以上分析对比,spdlog在性能和接口易用性上完胜其他日志框架,是日志框架的首选,其次boost.log也是一个不错选择,但是其接口用起来不是特别顺畅,对于log4cplus还是不推荐大家使用了,已经用了的尽快切换吧,当然spdlog使用了大量C++新特性,你必须使用C++11以上的新标准,但这似乎不是什么问题。