Введение
В заключительной статье цикла подведём практический итог — сравним производительность spdlog/fmt с qDebug()/std::cout на типичных сценариях, и соберём рекомендации по конфигурации логирования для production-окружения, основанные на темах, разобранных в предыдущих статьях цикла.
Концепция
Производительность логирования определяется тремя факторами: скоростью форматирования сообщения (fmt заметно быстрее std::ostringstream и сопоставим или быстрее QString::arg() для большинства сценариев), накладными расходами на саму запись (синхронная запись в файл — самая медленная операция, асинхронная запись из статьи 336 устраняет блокировку вызывающего потока), и эффективностью отбрасывания отфильтрованных по уровню сообщений (хорошая библиотека логирования должна минимизировать работу для сообщений, которые в итоге не будут выведены из-за настроек уровня).
Пример кода: Производительность
// Простой бенчмарк для сравнения подходов (концептуальный пример — для реального
// измерения стоит использовать специализированную библиотеку бенчмаркинга, например google benchmark)
#include <chrono>
#include <spdlog/spdlog.h>
void benchmarkFormatting()
{
const int iterations = 100000;
// std::ostringstream
auto start1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
std::ostringstream oss;
oss << "Устройство esp32_01: температура " << 23.5 << " влажность " << 45.0;
std::string result = oss.str();
}
auto duration1 = std::chrono::high_resolution_clock::now() - start1;
// fmt::format
auto start2 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
std::string result = fmt::format("Устройство {}: температура {} влажность {}",
"esp32_01", 23.5, 45.0);
}
auto duration2 = std::chrono::high_resolution_clock::now() - start2;
// Типичный практический результат (точные цифры зависят от компилятора и платформы):
// fmt::format заметно быстрее std::ostringstream, особенно при частых, повторяющихся вызовах
spdlog::info("ostringstream: {} мс, fmt::format: {} мс",
std::chrono::duration_cast<std::chrono::milliseconds>(duration1).count(),
std::chrono::duration_cast<std::chrono::milliseconds>(duration2).count());
}
// Рекомендованная production-конфигурация, объединяющая темы всего цикла
void setupProductionLogger()
{
spdlog::init_thread_pool(8192, 1); // статья 336: асинхронная запись
// Файловый sink с ротацией — детальный уровень для последующей диагностики
auto fileSink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"logs/app.log", 10 * 1024 * 1024, 5); // статья 336
fileSink->set_level(spdlog::level::debug);
fileSink->set_pattern("[%Y-%m-%d %T.%e] [%l] [%t] %v"); // %t — id потока, полезно для многопоточных приложений
// Консольный sink — только важные сообщения, для оперативного мониторинга при запуске вручную
auto consoleSink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
consoleSink->set_level(spdlog::level::warn);
auto logger = std::make_shared<spdlog::async_logger>(
"app", spdlog::sinks_init_list{fileSink, consoleSink},
spdlog::thread_pool(), spdlog::async_overflow_policy::block); // статья 336
logger->set_level(spdlog::level::debug);
logger->flush_on(spdlog::level::err); // немедленный сброс на диск при ошибках и выше —
// важные сообщения не должны зависеть от периодического flush
spdlog::register_logger(logger);
spdlog::set_default_logger(logger);
installQtToSpdlogBridge(); // статья 338 — для проектов, сохраняющих qDebug() в существующем коде
// Периодический flush для info/debug сообщений — компромисс между надёжностью
// (минимизация потери логов при сбое) и производительностью (не сбрасывать на диск при каждом сообщении)
spdlog::flush_every(std::chrono::seconds(3));
}
Пояснения к коду
Бенчмарк демонстрирует общий, ожидаемый паттерн — fmt::format() обычно показывает заметно лучшую производительность, чем std::ostringstream, для типичных сценариев форматирования с несколькими аргументами разных типов, что объясняется внутренней реализацией fmt, избегающей части накладных расходов потокового вывода std::ostream, на котором базируется std::ostringstream. setupProductionLogger() собирает воедино рекомендации из всего цикла — асинхронность (статья 336) для устранения блокировки вызывающего потока, ротация (статья 336) для управления размером файлов на диске, раздельная настройка уровней для файла и консоли (статья 335) для разделения подробной диагностики и оперативного мониторинга, flush_on(spdlog::level::err) для гарантированной записи важных сообщений немедленно (а не с задержкой, типичной для асинхронного режима), и периодический flush_every() для остальных, менее критичных уровней — баланс между надёжностью записи и производительностью.
Подводные камни
- Излишняя оптимизация форматирования строк в коде, где логирование составляет незначительную долю общей нагрузки приложения, без предварительного профилирования, подтверждающего, что именно логирование — реальное узкое место производительности — преждевременная оптимизация логирования в приложении, где основная нагрузка приходится на сетевые операции или обработку данных, малозначима и отвлекает усилия от действительно влияющих на производительность участков кода; решение о переходе на fmt/spdlog ради производительности стоит принимать на основе профилирования, а не предположений.
- Установка
flush_on()для слишком низкого уровня (например,infoвместоerr) в production-конфигурации с высокой интенсивностью логирования — частый принудительный сброс буфера на диск после каждого info-сообщения существенно снижает выгоду от асинхронного логирования, фактически возвращая часть накладных расходов синхронной записи;flush_on()стоит применять выборочно, для действительно критичных уровней (error/critical), оставляя менее важные сообщения периодическому, более редкому сбросу черезflush_every(). - Игнорирование результатов бенчмарка, специфичных для конкретной платформы и компилятора, и слепое перенесение общих «типичных» цифр производительности fmt/spdlog из документации или сторонних источников на собственный проект без локальной проверки — реальный выигрыш в производительности зависит от конкретного компилятора, уровня оптимизации сборки, и характера используемых форматов вывода; для проектов, где производительность логирования действительно критична, стоит провести собственное, локальное измерение на актуальной конфигурации, а не полагаться исключительно на общие цифры из внешних источников.
- Отсутствие явного вызова
spdlog::shutdown()при завершении приложения (аналогично подводному камню из статьи 336), что в production-конфигурации с асинхронным логированием особенно важно — без этого вызова часть сообщений, особенно сформированных непосредственно перед завершением процесса (что нередко происходит именно в момент сбоя или экстренного завершения, наиболее ценных для последующей диагностики), может быть потеряна без записи на диск, что подрывает саму цель ведения подробных логов для последующего анализа инцидентов.
Цикл «spdlog и fmt: логирование и форматирование строк» завершён — общий план теперь включает 340 статей, дополняя темы межсервисного взаимодействия (gRPC/Protobuf) практическим инструментарием диагностики и наблюдаемости production-приложений.