对一些知名或以低延时为目标的 C/C++ 日志库做的 Benchmark

2023-10-17 11:44:49 +08:00
 weidaizi

项目地址:https://github.com/MuggleWei/cc_log_benchmark

概述

当前已经有许多开源的 C/C++ 日志库, 本项目对一些知名或以低延时为目标的日志库做一个 Benchmark, 力求发现当面对如 MMORPG Server 或是金融交易系统这类低延时的场景下, 哪些日志库是较好的选择.

省流,将结果放在前面

提醒: 有一些图表之外需要注意的内容, 感兴趣的读者可在最后看到

日志库列表

此库对几个 C/C++ 日志库做 benchmark, 被测的库如下所示(按字母顺序排序)

  • easyloggingpp: C++ logging library. It is extremely powerful, extendable, light-weight, fast performing, thread and type safe and consists of many built-in features. It provides ability to write logs in your own customized format. It also provide support for logging your classes, third-party libraries, STL and third-party containers etc.
  • fmtlog: fmtlog is a performant fmtlib-style logging library with latency in nanoseconds.
  • glog: C++ implementation of the Google logging module
  • haclog: Haclog(Happy Aync C log) is an extremely fast plain C logging library
  • loguru: A lightweight C++ logging library
  • Nanolog: Nanolog is an extremely performant nanosecond scale logging system for C++ that exposes a simple printf-like API.
  • quill: Asynchronous Low Latency C++ Logging Library
  • reckless: Reckless logging. Low-latency, high-throughput, asynchronous logging library for C++.
  • spdlog: Fast C++ logging library

Benchmark 说明

使用 google benchmark 进行测试, 测试分为两部分场景

  1. 场景 1: 设定最小的测试时间 (设置 google benchmark 中的 MinTime), 在这时间中尽可能的压力测试写日志, 此场景主要针对异步日志, 可以反映日志库的吞吐量, 以及当缓冲区高度紧张的情况下日志前端的写入效率如何. 在此场景下测试的线程数量为: 1/2/4/8
  2. 场景 2: 设定迭代和重复次数 (设置 google benchmark 中的 Iterations + Repetitions), 反映在非压力测试的情况下, 各个日志库的表现如何. 在此场景下测试的线程数量为: 1/${0.5 * CPU 数量}/${1 * CPU 数量}/${2 * CPU 数量}

基准测试

测试日期: 2023-10-14
被测库版本, 详见: CMakeLists.txt
单次写入数据:

struct LogMsg {
	uint64_t u64;
	uint32_t u32;
	int64_t i64;
	int32_t i32;
	char s[128];
};

日志输出格式:

${Level}|${datetime}|${filename}.${line_no}|${func_name}|${thread_id} - u64: msg.u64, i64: msg.i64, u32: msg.u32, i32: msg.i32, s: msg.s

测试机器

类型: 笔记本
机器: 20R10002CD ThinkPad X1 Carbon 7th
系统: Arch Linux x86_64
Kernel: 6.5.6-arch2-1
CPU: Intel i7-10710U (12) @ 4.700GHz
GPU: Intel Comet Lake UHD Graphics
Memory: 15659MiB
gcc: (GCC) 13.2.1 20230801
ldd: (GNU libc) 2.38

额外说明

由于我本地机器的测试中遇到一些问题, 导致 fmtlog, quill 和 reckless 没有做到测试场景完全的覆盖

  • 当 fmtlog 加入 #define FMTLOG_BLOCK 1 时, 测试进程会被卡死而无法继续, 所以 fmtlog 只测试了缓冲区满选择丢弃日志的模式
  • 当 quill 使用 UNBOUNDED 时, 在 场景 1 中, 会导致内存使用一直增长而失败, 所以在测试 quill_unbounded 的时候, 跳过 场景 1 的测试
  • reckless 在 场景 1 中会卡住, 所以 reckless 跳过 场景 1 的测试
  • fmtlog 和 Nanolog 我并没有找到设置默认缓冲区的接口, 所以都只使用了默认的缓冲区大小

由于个人水平所限, 若有错误和纰漏, 亦或是考虑欠妥之处, 还请不吝指正!

结果分析

通过上述图表不难看出

  • 在场景 1 中, 较快的日志库有(按字母顺序排序): fmtlog, haclog, Nanolog, quill_bounded
  • 在场景 2 中, 较快的日志库为(按字母顺序排序): fmtlog, haclog, Nanolog

那么上面提到的这四个日志库为什么更快?他们有什么优劣势,以及使用中有什么要注意的坑呢?

以上 4 个日志库均为异步日志库, 设计上也都是多缓冲区队列写, 消费者负责轮询的模式, 整体思路相似, 代码细节各有各的趣味. 但作为用户来说, 有一点要特别注意

特别注意!!!
特别注意!!!
特别注意!!!

既然都使用了缓冲区, 那么就一定要考虑缓冲区被写满的情况, 此时有三种不同的应对方式

  1. 阻塞: 生产者线程阻塞, 等待缓冲区有足够空间后再写入
  2. 丢弃: 放弃写入本条日志, 或者直接套圈写入本条日志但是放弃一整个缓冲区的日志
  3. 扩展: 动态增加缓冲区的长度, 继续写入

haclog 与 Nanolog 选择了方案 1, fmtlog 可选方案 1/2, quill 可选方案 2/3
而由于 额外说明 中提到的情况, 导致 fmtlog 只可选择方案 2, quill 在 场景 1 中仅讨论 bounded 模式

fmtlog

优点

  • 在两种场景下都表现出良好的速度, 其中在 场景 1 中速度与 quill_bounded 旗鼓相当, 并列最快速度
  • 在两种场景下, 速度表现的稳定性足够好, 波动较小
  • 采用了 format 格式化风格

缺点

  • 场景 1 中出现了大量丢失日志的情况
  • 场景 2 中也出现了日志丢失的情况
  • 设置 #define FMTLOG_BLOCK 1 情况时, 无法完成 场景 1 的测试

haclog

优点

  • 在两种场景下大部分情况都表现出良好的速度, 其中 场景 2 中多线程的情况下, 速度仅略逊于 Nanolog
  • 缓冲区满时采用了阻塞模式, 不会丢日志

缺点

  • 当吞吐量超过了某个阈值, 缓冲区大量被写满的情况下, 效率会出现大幅下跌

Nanolog

优点

  • 在两种场景下都表现出良好的速度, 其中在 场景 2 中多线程的情况下, 速度排名第一
  • 很高的吞吐量, 无论在 场景 1 还是 场景 2 中, 都没有出现效率大幅下跌的情况
  • 缓冲区满时采用了阻塞模式, 不会丢日志

缺点

  • 日志无法直接阅读, 需要使用附带的 decompressor 程序进行解码
  • 仅支持 Linux

quill

优点

  • 场景 1 中速度与 fmtlog 旗鼓相当, 并列最快速度
  • 采用了 format 格式化风格

缺点

  • 在 bounded 模式下会出现日志丢失的情况
  • 在 unbounded 模式下, 有可能导致内存一直增长, 无法完成 场景 1 的测试

额外的一点考虑

haclog 使用纯 C 开发, 而 fmtlog, Nanolog 和 quill 采用 C++ 开发; 当就日志库这个场景来说, 理论上 C++ 实现速度上限会更高一点点, 表现为以下两个方面

  • 编译期计算: C++ 能在编译期预计算好日志参数信息, 而纯 C 实现的日志库需要在第一次运行时计算
  • 日志前端序列化:
    • C 需要在运行时遍历 va_list, 生成的汇编代码是通过循环来逐个参数序列化, 无论是通过 switch 判断预计算好的类型,或是调用预先设置好的函数指针, 都可能会带来一点额外的开销
    • C++ 通过变参模板来进行序列化, 汇编代码可实现为平铺展开的形式, 以空间换时间

综上所述

  • fmtlog 和 quill 采用 format 格式化风格, 书写方便, 且两者速度稳定性较强, fmtlog 和 quill_bounded 在 场景 1 表现出色. 但是如上一小节所分析的, 他们的速度部分是通过日志丢失换取的, 这在使用场景上需要特别注意
  • haclog 在两种场景下也都表现出色, 在 场景 2 中的表现仅略逊与 Nanolog, 但是在 场景 1 当缓冲区被压满的情况下, 效率大幅度的降低. 这正好与 fmtlog 以及 quill 相反, 他以速度降低来换取了不丢日志
  • Nanolog 在两种场景下都表现出极高的效率, 以及超高的吞吐量. 在 场景 2 下的王者, 在 场景 1 中面临压力的情况下, 保证不丢日志也不用拿太多的效率去换. 不过其超高的吞吐量, 是拿日志非实时可读换取的, 这也导致诸如 tail 一类的工具无法使用

可以看出, 当前这几个异步日志并没有一个能在全方位碾压其他日志库, 而是都在某一方面做了权衡取舍

2047 次点击
所在节点    程序员
8 条回复
liprais
2023-10-17 11:56:55 +08:00
你用了一个带睿频的 cpu 测试的么?
cnbatch
2023-10-17 12:08:43 +08:00
看完这个测试后,OP 的测试应该是测出了日志库的不可能三角?
高吞吐、不丢日志、可实时读换
weidaizi
2023-10-17 13:15:58 +08:00
@liprais 是的,支持睿频,在测试之前,我已经直接 cpupower 设置成了 performance
weidaizi
2023-10-17 13:17:55 +08:00
@cnbatch 👍 哈哈哈,这位朋友总结的专业!
更严谨一点,应该是: 高吞吐 -> 低时延且高吞吐
kkocdko
2023-10-17 19:25:56 +08:00
一楼的意思是你应该把 boost=0 ,睿频会有很大的性能波动。benchmark 这种事情本身就是求稳定,才能对各个库都公平。
weidaizi
2023-10-17 23:18:55 +08:00
@kkocdko 谢谢您的提醒!
我之前一直认为,设置了 `cpupower frequency-set -g performance`, 那么默认都应该是固定在最高频率上跑,频率应该是稳定的;您要表达的意思是,如果 turbo boost 开着,我这样设置之后频率依然是不稳定的吗?
weidaizi
2023-10-18 00:29:19 +08:00
@kkocdko @liprais 👍 查了一番,我之前的理解的确不到位,通过 cpupower 设置为 performance 在睿频开启的情况下的确还是有波动,谢谢两位!之后我会重新出一个关闭了 Turbo Boost 的结果
weidaizi
2023-10-18 23:14:48 +08:00
@kkocdko @liprais 更新了一下 benchmark ,增加了 `CPU 频率设置与监控`,固定在 3.2GHz 上测试,再次感谢两位的提醒! 😀

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/982706

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX