Асинхронное логирование в C++ — не серебряная пуля
Асинхронное логирование C++ часто воспринимается как очевидное улучшение: вынесли запись в отдельный поток — и проблема решена.
На практике это почти никогда не работает так просто.
Если посмотреть на реальные системы, становится видно, что асинхронное логирование C++ не убирает стоимость логирования — оно лишь перераспределяет её между потоками и этапами обработки. Более того, в некоторых случаях оно делает систему сложнее и даже медленнее.
В этой статье разберёмся, что именно происходит внутри async logging, где тратится CPU и почему «вынести в отдельный поток» — далеко не универсальное решение.
Почему логирование становится проблемой
Когда система начинает логировать много, неожиданно оказывается, что логирование — это не «бесплатная операция».
Есть три основных источника затрат:
— форматирование строк
— синхронизация между потоками
— ввод-вывод (I/O)
Интуитивно кажется, что основной bottleneck — диск. Но на практике часто оказывается наоборот: значительная часть CPU уходит на форматирование и блокировки.
Именно поэтому асинхронное логирование C++ не всегда даёт ожидаемый выигрыш.
Как устроено асинхронное логирование C++
Асинхронное логирование C++ — это pipeline из нескольких этапов:
- Захват данных (format + аргументы или готовая строка)
- Помещение в очередь
- Обработка backend-потоком
- Запись в 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 показывают отличные результаты в синхронном режиме, иногда сопоставимые или лучше полностью асинхронных решений.
При этом это не «чистый 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 легко превращается в усложнение архитектуры без реального выигрыша.