Site icon Заметки разработчика

Асинхронное логирование в C++ — не серебряная пуля

асинхронное логирование C++ benchmark logme spdlog quill

Асинхронное логирование C++ часто воспринимается как очевидное улучшение: вынесли запись в отдельный поток — и проблема решена.

На практике это почти никогда не работает так просто.

Если посмотреть на реальные системы, становится видно, что асинхронное логирование C++ не убирает стоимость логирования — оно лишь перераспределяет её между потоками и этапами обработки. Более того, в некоторых случаях оно делает систему сложнее и даже медленнее.

В этой статье разберёмся, что именно происходит внутри async logging, где тратится CPU и почему «вынести в отдельный поток» — далеко не универсальное решение.

Почему логирование становится проблемой

Когда система начинает логировать много, неожиданно оказывается, что логирование — это не «бесплатная операция».

Есть три основных источника затрат:

— форматирование строк
— синхронизация между потоками
— ввод-вывод (I/O)

Интуитивно кажется, что основной bottleneck — диск. Но на практике часто оказывается наоборот: значительная часть CPU уходит на форматирование и блокировки.

Именно поэтому асинхронное логирование C++ не всегда даёт ожидаемый выигрыш.

Как устроено асинхронное логирование C++

Асинхронное логирование C++ — это pipeline из нескольких этапов:

  1. Захват данных (format + аргументы или готовая строка)
  2. Помещение в очередь
  3. Обработка backend-потоком
  4. Запись в sink (файл, консоль и т.д.)

Важно: ни один из этих этапов не исчезает.

Асинхронность лишь добавляет:

— очередь
— дополнительную синхронизацию
— ещё один поток

Отложенное форматирование

Популярная идея — отложить форматирование до backend-потока.

На практике это упирается в lifetime данных.

Пример:

std::string s = GetString();
LogInfo(«value: {}», s);

Если сохранить только ссылку на s, то к моменту форматирования она уже может быть невалидной.

Варианты:

— копировать данные
— форматировать сразу
— требовать сериализацию

Ни один из них не является идеальным.

В реальных системах это часто приводит к неожиданному результату: отложенное форматирование может быть дороже синхронного.

Очередь — это не ускорение

Ключевой момент:

асинхронное логирование C++ не ускоряет систему само по себе.

Если система может обработать 100k сообщений/сек, а генерируется 1M — async не решает проблему.

Он лишь меняет поведение:

— sync → тормозит сразу
— async → накапливает очередь

То есть создаётся накопленный долг.

И он проявляется как:

— рост памяти
— блокировки
— потеря сообщений

Реальные измерения

Подробные результаты benchmark приведены в предыдущей статье: https://habr.com/ru/articles/1012874/

В тесте сравнивались популярные библиотеки:

logme
spdlog
quill

Ключевое наблюдение

logme и spdlog показывают отличные результаты в синхронном режиме, иногда сопоставимые или лучше полностью асинхронных решений.

При этом это не «чистый sync».

Фактически архитектура такая:

— синхронное форматирование
— минимальный overhead в вызове
— буферизованная запись в файл вне критического пути

То есть выигрыш достигается не за счёт async, а за счёт того, что базовый путь уже очень дешёвый.

Что показывают цифры

— logme быстрее spdlog примерно на ~15% в file сценарии
— в null-сценарии различия достигают порядков
— форматирование может замедлять логирование в 3–5 раз

При этом полностью асинхронные решения (например, quill) не всегда выигрывают по throughput.

Почему так происходит

Async добавляет накладные расходы:

— очередь
— синхронизацию
— дополнительный поток

Если bottleneck уже находится в форматировании или contention — это не помогает.

На практике асинхронное логирование C++ часто используется без понимания реальных bottleneck’ов.

Пример: где теряется производительность

void Log(const char* fmt, …)
{
  char buffer[16384];

  va_list args;
  va_start(args, fmt);

  vsnprintf(buffer, sizeof(buffer), fmt, args);

  queue.push(buffer);

  va_end(args);
}

Здесь уже есть:

— форматирование
— копирование
— синхронизация

Даже без I/O.

Когда асинхронное логирование C++ действительно помогает

Асинхронное логирование C++ полезно, когда:

— важна минимизация latency
— есть burst-нагрузка
— I/O действительно является bottleneck

Когда асинхронное логирование C++ делает хуже

Есть случаи, где async — плохой выбор:

— низкая нагрузка
— debugging (важен порядок сообщений)
— crash-сценарии
— перегруженные backend-потоки

В этих ситуациях sync logging может быть проще и быстрее.

Асинхронное логирование C++ — вывод

Асинхронное логирование C++ — это не универсальная оптимизация.

Это перераспределение затрат.

И правильный вопрос звучит так:

где именно тратится время в вашей системе?

Без этого понимания выбор между sync и async легко превращается в усложнение архитектуры без реального выигрыша.

Exit mobile version