Некоторые из перечисленных.
Рекомендую потратить время и просмотреть доклад с cppcon-а, где докладчик рассказывает как работает его логгер. У него очень быстрый логгер для low latency систем.
Про выделение памяти явное и неявное — все верно. std::format, насколько я понимаю, доступен только с С++20, а у меня сейчас потолок С++17, да и основной формат вывода все таки потоковый.
Тут имелось ввиду использование в целом printf-style форматитирование.
Допустим, я буду использовать кольцевой буфер. Как определиться с его размером? Длина лог-сообщений в моих задачах варьируется от 100 символов, до 5Мб символов (бывает и больше).
Для начало стоит сказать, что ваш логгер форматирует строку в вызывающем потоке. Это медленно и этого можно избежать. Да, это можно сделать с ostream-style логгером, но с printf-style сделать будет проще и без костылей. Проще, потому что на момент логгирования можно подсчитать сколько нужно байт для логгирования (например: sizeof(указатель на метаданные: fmtstring, loglevel, etc) + sizeof (timestamp type) + sizeof (для тривиальных типов) + strlen(для строковых)) и очень часто размер известен на момент компиляции (жаль что в c++ нет constexpr аргументов, можно было бы сделать с гарантиями). Далее все это memcpy в ring buffer. Форматирование и другая обработка делается в отдельном потоке.
Да, тут нужно сделать оговорку, что ring buffer может переполниться, если пишущий поток медленно вычитывает. Но думаю это решаемо и в общем случае это увеличение размера ring buffer-а. В реальном приложении врятли получится создать такую нагрузку, что бы начать переписывать ring buffer по кругу.
Не очень понятна мысль про N очередей для N потоков. Вы предлагаете для каждого рабочего потока иметь свою очередь?
Именно, т.к. меньше потоков (логгирующих потоков) будут иметь доступ к примитиву синхронизации. А примитивы так или иначе точно будут. Даже спинлок будет давать задержку, если к ниму одновременно будут обращаться N потоков.
Несколько советов по улучшению:
1. ostream-style логгирование — это медленно. Особенно если логировать произвольные типы, которые умеют сериализоваться в std::ostream.
2. В вашем логгере в каждой вызове явное выделение памяти в Line (new Impl) + выделение памяти в деструкторе Line (new Message) + неявнЫЕ в std::string. Лучше использовать что-то типа ring buffer queue. Имея синтаксис printf/std::format практически всегда можно вычислить необходимое количество байт для передачи через ring buffer (есть нюансы).
3. Вместо спинлока в Logger::addMessage можно сделать N очередей для N потоков. Лок, в данном случае, будет только в момент создания очереди. В потоках использовать thread local storage.
А я решил начать с ortho 5x15, т.к. не хотелось терять "[", "]" на основном слое. Долго не мог с раскладкой разобраться, но в итоге придумал. Оказалось, что enter, del, backspace и другие клавиши удобно иметь в нижнем ряду. Фишка подобных клав — это QMK, По-моему с помощью данного фреймворка можно настроить вообще все что угодно. Например, на одну клавишу можно назначить разное поведение — пробел при кратковременном нажатии и, допустим, shift при удержании.
Почему нет гарантий? std::atomic — это про память и инструкции процессора. И, насколько мне известно, стандарт не регламентирует в какой именно памяти должна быть расположена atomic переменная. Кмк тут все зависит от конкретной имплементации std::atomic.
fmtlib только валидирует формат строку, без какой либо compile-time магии со строками.
Ну и spdlog достаточно медленный для моих задач. Форматирование inplace — это дорого.
Рекомендую потратить время и просмотреть доклад с cppcon-а, где докладчик рассказывает как работает его логгер. У него очень быстрый логгер для low latency систем.
Тут имелось ввиду использование в целом printf-style форматитирование.
Для начало стоит сказать, что ваш логгер форматирует строку в вызывающем потоке. Это медленно и этого можно избежать. Да, это можно сделать с ostream-style логгером, но с printf-style сделать будет проще и без костылей. Проще, потому что на момент логгирования можно подсчитать сколько нужно байт для логгирования (например: sizeof(указатель на метаданные: fmtstring, loglevel, etc) + sizeof (timestamp type) + sizeof (для тривиальных типов) + strlen(для строковых)) и очень часто размер известен на момент компиляции (жаль что в c++ нет constexpr аргументов, можно было бы сделать с гарантиями). Далее все это memcpy в ring buffer. Форматирование и другая обработка делается в отдельном потоке.
Да, тут нужно сделать оговорку, что ring buffer может переполниться, если пишущий поток медленно вычитывает. Но думаю это решаемо и в общем случае это увеличение размера ring buffer-а. В реальном приложении врятли получится создать такую нагрузку, что бы начать переписывать ring buffer по кругу.
Именно, т.к. меньше потоков (логгирующих потоков) будут иметь доступ к примитиву синхронизации. А примитивы так или иначе точно будут. Даже спинлок будет давать задержку, если к ниму одновременно будут обращаться N потоков.
Это как раз нормально. Меньше примитивов синхронизации — меньше накладных расходов на логгироаание в вызывающем потоке.
Несколько советов по улучшению:
1. ostream-style логгирование — это медленно. Особенно если логировать произвольные типы, которые умеют сериализоваться в std::ostream.
2. В вашем логгере в каждой вызове явное выделение памяти в Line (new Impl) + выделение памяти в деструкторе Line (new Message) + неявнЫЕ в std::string. Лучше использовать что-то типа ring buffer queue. Имея синтаксис printf/std::format практически всегда можно вычислить необходимое количество байт для передачи через ring buffer (есть нюансы).
3. Вместо спинлока в Logger::addMessage можно сделать N очередей для N потоков. Лок, в данном случае, будет только в момент создания очереди. В потоках использовать thread local storage.
Поправка:
в c++20 должно работать
Почему нет гарантий? std::atomic — это про память и инструкции процессора. И, насколько мне известно, стандарт не регламентирует в какой именно памяти должна быть расположена atomic переменная. Кмк тут все зависит от конкретной имплементации std::atomic.
А если send вернет больше 0 или 0?
Зачем для последнего кейса шаблон?> ./logging-bench
2019-01-31 18:37:47
Running ./logging-bench
Run on (8 X 4200 MHz CPU s)
CPU Caches:
L1 Data 32K (x4)
L1 Instruction 32K (x4)
L2 Unified 256K (x4)
L3 Unified 8192K (x1)
***WARNING*** Library was built as DEBUG. Timings may be affected.
--------------------------------------------------------------
Benchmark Time CPU Iterations
--------------------------------------------------------------
bmLoggingSingleString 121 ns 120 ns 5835101
bmLogging1Arg 134 ns 134 ns 5213675
bmLogging2Arg 156 ns 155 ns 4504116
bmLogging3Arg 218 ns 215 ns 3255618
bmLogging5StringArgs 538 ns 526 ns 1346569
bmLogging2ArgWithSpecs 168 ns 164 ns 4299307
Это в дебаге, в релизе на порядок (или 2) меньше
Да, с минимальной поддержкой типов и флагов форматирования. Тут фишка в том, что форматирование (да и запись в файл) происходит в отдельном процессе.
Ну и spdlog достаточно медленный для моих задач. Форматирование inplace — это дорого.