Я неопытный разработчик и заметил, что spdlog позволяет создавать экземпляры многопоточных регистраторов, используя spdlog::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
, как показано выше. Результаты показывают, что версия asyn c работает значительно хуже, чем обычная версия с резьбой. Насколько я могу судить, синхронизация происходит в элементе q_type
пула потоков , то есть mpmc_blocking_queue
. Очередь синхронизирует доступ через один мьютекс, что может ухудшить производительность в этом синтетическом тесте c с 10 производственными потоками и 1 потребительским потоком. Реальные системы могут демонстрировать очень различное поведение, но пропускная способность асинхронной версии составляет только половину пропускной способности многопоточной версии даже при наличии только одного потока производителя. С экспериментальными изменениями, приведенными ниже, асинхронная версия достигает почти одинаковой производительности для 1 потока и значительно лучшей производительности для 10 потоков.
Экспериментальное улучшение с использованием свободной от блокировки ограниченной очереди
********** 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
Кому подтвердите предположение, что я использовал реализацию ограниченной очереди без блокировки , предоставленную Дмитрием Вьюковым в его блоге, которую я немного изменил. В очереди не реализована блокирующая операция enqueue
и синхронная dequeue_for
операция. В моем адаптере MPMCQueueAdapter
для этой очереди я просто использовал простое вращение с некоторым механизмом отката для реализации требуемого ожидания. Очередь может быть довольно быстрой, но атомы C ++ не предоставляют стандартный механизм ожидания до C ++ 20 . Исходное неблокирующее enqueue_nowait
демонстрировало другое поведение в том смысле, что оно заменило самую старую запись в очереди, в то время как мой адаптер просто пытается поставить сообщение в очередь и ничего не делает в случае его заполнения. Из-за этих предостережений, это только для справки по производительности, чтобы определить, насколько конфликт может быть причиной потока.
Использование очереди без блокировки под колпаком значительно повышает производительность, хотя эталонный тест - синтетический c и такая высокая конкуренция может не произойти в реальных системах. Суть реализации по умолчанию может заключаться в том, что потребитель (поток, который пересылает сообщение в реальный приемник) синхронизируется с тем же мьютексом, что и производители, или мои тесты просто каким-то образом глубоко испорчены. Что вы думаете?
Тестовая система
- ОС: Windows 10 x64 Версия 1903
- Компилятор: Visual Studio 2019 16.4
- Ram : 16 ГБ DDR4 3200
- ЦП: Ryzen 2600 6C / 12T
- SSD: Samsung 850 EVO 250 ГБ
РЕДАКТИРОВАТЬ: я использовал размер очереди 32768 для теста результаты выше вместо первоначально заявленных 16384.