Различные статьи по Qt и c++

Производительность: сравнение spdlog/fmt с qDebug и std::cout, советы по продакшен-конфигурации

Введение

В заключительной статье цикла подведём практический итог — сравним производительность 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-приложений.