我是一个缺乏经验的开发人员,并注意到spdlogspdlog::async_factory
允许使用作为第一个模板参数来实例化多线程记录器。
auto basic_async_mt = spdlog::basic_logger_mt<spdlog::async_factory>("basic_mt", "logs/basic_mt.log", true);
// vs
auto basic_mt = spdlog::basic_logger_mt("basic_mt", "logs/basic_mt.log", true);
问题
鉴于我下面的测试结果,异步日志记录比同步多线程版本慢,并且随着争用的增加而扩展得更糟,异步实现的目的是什么?
简单的基准测试
- 结果:使用默认队列进行基准测试
********** Single threaded: 1000000 messages **********
basic_st Elapsed: 0.45 secs 2239158/sec
rotating_st Elapsed: 0.44 secs 2293912/sec
daily_st Elapsed: 0.45 secs 2210196/sec
********** Multi threaded: 1 threads, 1000000 messages **********
basic_mt Elapsed: 0.41 secs 2458098/sec
rotating_mt Elapsed: 0.45 secs 2238492/sec
daily_mt Elapsed: 0.48 secs 2074991/sec
********** Multi threaded: 10 threads, 1000000 messages **********
basic_mt Elapsed: 1.05 secs 950041/sec
rotating_mt Elapsed: 1.02 secs 977636/sec
daily_mt Elapsed: 1.11 secs 900860/sec
********** Async threaded: 1 threads, 1000000 messages **********
basic_mt Elapsed: 0.79 secs 1259206/sec
rotating_mt Elapsed: 1.00 secs 1002389/sec
daily_mt Elapsed: 0.84 secs 1197042/sec
********** Async threaded: 10 threads, 1000000 messages **********
basic_mt Elapsed: 2.56 secs 390791/sec
rotating_mt Elapsed: 2.72 secs 367184/sec
daily_mt Elapsed: 2.72 secs 368253/sec
由于存储库包含一些简单的基准测试,我首先运行了bench并进行了一些修改:我将线程池队列大小设置为 32768,并使用spdlog::init_thread_pool(1 << 15, 1);
. 我进一步添加了一个使用模板参数的bench_async_logging
版本,如上所示。结果表明,异步版本的性能明显低于普通线程版本。据我所知,同步发生在线程池的成员中,这是一个bench_threaded_logging
spdlog::async_factory
q_type
mpmc_blocking_queue
. 队列通过单个互斥体同步访问,这可能会降低此合成基准测试中的性能,其中包含 10 个生产者线程和 1 个消费者线程。实际系统可能表现出非常不同的行为,但异步版本的吞吐量只有多线程版本的一半,即使只有 1 个生产者线程。通过下面的实验性更改,异步版本在 1 个线程上实现了几乎相同的性能,在 10 个线程上实现了明显更好的性能。
使用无锁有界队列的实验改进
- 来源:https ://github.com/Araeos/spdlog/tree/async-lock-free (见include/mpmp_queue.hpp)
- 结果:使用无锁队列进行基准测试
********** Single threaded: 1000000 messages **********
basic_st Elapsed: 0.44 secs 2248376/sec
rotating_st Elapsed: 0.45 secs 2217191/sec
daily_st Elapsed: 0.48 secs 2092311/sec
********** Multi threaded: 1 threads, 1000000 messages **********
basic_mt Elapsed: 0.42 secs 2395124/sec
rotating_mt Elapsed: 0.45 secs 2242889/sec
daily_mt Elapsed: 0.51 secs 1960067/sec
********** Multi threaded: 10 threads, 1000000 messages **********
basic_mt Elapsed: 0.98 secs 1019659/sec
rotating_mt Elapsed: 1.28 secs 782557/sec
daily_mt Elapsed: 1.14 secs 879411/sec
********** Async threaded: 1 threads, 1000000 messages **********
basic_mt Elapsed: 0.50 secs 2004096/sec
rotating_mt Elapsed: 0.56 secs 1781594/sec
daily_mt Elapsed: 0.49 secs 2053135/sec
********** Async threaded: 10 threads, 1000000 messages **********
basic_mt Elapsed: 0.62 secs 1604676/sec
rotating_mt Elapsed: 0.71 secs 1415312/sec
daily_mt Elapsed: 0.73 secs 1378370/sec
为了验证假设,我使用了Dmitry Vyukov 在他的博客上提供的无锁有界队列实现,我稍微修改了它。队列没有实现阻塞enqueue
操作,也没有定时dequeue_for
操作。在我MPMCQueueAdapter
的这个队列的适配器中,我只是使用简单的旋转和一些退避机制来实现所需的等待。队列可能很快,但 C++ 原子直到 C++20 才提供标准等待机制。原始的非阻塞enqueue_nowait
表现出不同的行为,因为它替换了队列中最旧的条目,而我的适配器只是尝试将消息排入队列并且如果已满则不执行任何操作。由于这些注意事项,这仅用于性能参考,以了解有多少线程争用可能是这里的一个因素。
尽管基准测试是综合的,并且在实际系统中可能不会发生如此高的争用,但在底层使用无锁队列可以显着提高性能。默认实现的关键可能只是消费者(将消息转发到实际接收器的线程)与生产者相同的互斥锁同步,或者我的测试在某些方面存在严重缺陷。你觉得呢?你有没有什么想法?
测试系统
- 操作系统:Windows 10 x64 版本 1903
- 编译器:Visual Studio 2019 16.4
- 内存:16GB DDR4 3200
- CPU:锐龙2600 6C/12T
- 固态硬盘:三星 850 EVO 250GB
编辑:我为上面的基准测试结果使用了 32768 的队列大小,而不是最初声明的 16384。