Skip to content

Performance

Eduard Mishkurov edited this page Jul 26, 2026 · 4 revisions

Performance

Performance is one of the main design goals of logme. The important point is that logme is not fast only when a message is actually written. It is also optimized for the common production case where detailed logging exists in the source code but is disabled, filtered out, or routed only when diagnostics are required.

The performance results below are taken from the separate logbench repository. That repository contains the benchmark source code, build scripts, an article, and charts, so the numbers are reproducible rather than being hand-written claims in this wiki.

The latest table below shows the number of completed benchmark cycles during the configured benchmark interval. Higher is better.

Library null file console file+console
logme (c) 1,121,998,419 153,752,170 325,518 359,772
logme (cpp-stream) 1,053,074,266 24,290,646 335,276 364,349
logme (std::format) 1,082,512,863 90,255,191 362,369 365,770
spdlog 244,991,156 118,546,708 368,944 355,449
quill 1,568,610 1,357,532 191,531 200,378
easylogging++ 25,597,177 1,656,726 356,948 277,846

These results are intentionally shown together with the benchmark repository link. Extremely large differences are easy to misread if the benchmark setup is not visible. For methodology, command lines, charts, and the full discussion, see the logbench article: docs/article.md.

What the numbers show

The null column measures the hot path where logging calls exist but no output is produced. This is a critical production scenario: detailed diagnostic points can remain in the code, but the application should not pay formatting and I/O costs when those messages are not needed.

In this mode, logme's C-style, stream-style, and std::format-style APIs are all above one billion completed cycles in the benchmark interval. The result reflects the architecture: early checks, cheap filtering, and per-call-site cached context are used to avoid repeated setup work.

The file column measures output to file. logme's C-style API is especially strong here because it combines a low-cost formatting path with asynchronous file output. Stream-style logging is still fast in the disabled/null case, but it is naturally more expensive when actual message construction is required.

The console and file+console columns are much closer across libraries because console output itself becomes the limiting factor. logme still remains competitive while preserving ordering and routing behavior.

Why logme is fast

logme's performance is not based on a single benchmark shortcut. Several mechanisms work together.

Compile-time removal

When _LOGME_ACTIVE is false, the macros compile to an inert branch. No logging work is performed at runtime because the call site is effectively removed.

Early runtime precheck

For calls that provide an explicit ChannelPtr, and for Do macros that can resolve the channel before preparation code runs, Detail/Precheck.h can reject the call before expensive user expressions are evaluated.

This does not mean that every disabled log call is magically free. It means that hot paths can be written so that filtering happens before formatting and before expensive argument preparation.

Call-site caching

Macros use a static ContextCache per call site. This reduces repeated setup work for metadata, formatting state, and context preparation. A logging call that is executed many times does not have to rediscover the same static information on every iteration.

Multiple formatting paths

logme supports C-style formatting, C++ stream-style output, and optional {} formatting through the configured format backend. These modes have different costs, and the benchmark reports them separately instead of hiding the difference behind one generic number.

Asynchronous file output

FileBackend batches data through BufferQueue and a shared file-manager thread. This avoids forcing every producer thread to synchronously perform physical file I/O for every line.

Runtime routing without permanent noise

The channel/subsystem/backend model allows expensive diagnostics to exist in the code but remain inactive until a specific channel or subsystem is enabled. This is one of logme's core production use cases: detailed logs are available on demand without permanently increasing output volume.

Profiling logging overhead in a running process

A CPU or I/O profiler can show that time is spent in FileBackend, queue synchronization, formatting, or operating-system write calls. It normally cannot identify which source statements caused that work.

The on-demand logstat profiler fills that gap:

logmectl -p 7791 logstat start

# Reproduce the workload.

logmectl -p 7791 logstat stop
logmectl -p 7791 logstat outputs --backend FileBackend --sort bytes --limit 30
logmectl -p 7791 logstat outputs --backend FileBackend --sort records --limit 30

This separates two different performance problems:

  • a small number of large records or payload dumps;
  • a very large number of small records that spend CPU on formatting, queueing, synchronization, and worker wakeups.

Collection is disabled by default. While disabled, the profiler performs no call-site registration, counter updates, locking, allocation, time queries, or metadata copying. For an already accepted record, the source-site and built-in backend paths add a relaxed atomic pointer load and a normally predicted null branch. Calls rejected by the existing early filters do not reach those checks.

See Log Source Profiling for backend fan-out, asynchronous file-worker statistics, result interpretation, and the full idle-versus-load workflow.

What adds overhead

The following features are intentionally optional because they have a cost:

  • std::format formatting
  • C++ stream message construction when output is actually produced
  • per-message overrides and rate limiting
  • display filters and global condition callbacks
  • subsystem checks
  • obfuscation/encryption for file output
  • console highlighting and extra output fields

Practical guidance

For hot paths:

  • keep expensive argument computation inside logging macros or Do forms
  • prefer explicit ChannelPtr logging when you want early precheck to help
  • use C-style logging for the lowest active formatting overhead
  • use stream-style logging when readability and incremental construction matter more than raw active-output throughput
  • disable unnecessary output fields
  • use FileBackend when batching matters
  • call Flush() only when you truly need a synchronization point

What logme does not claim

logme is not presented as:

  • fully lock-free
  • allocation-free in all formatting modes
  • async-signal-safe
  • faster than the console or the operating system's I/O path

The benchmark should be read as a reproducible measurement of the tested scenarios, not as a universal promise for every application. The important design goal is more practical: keep detailed logging cheap enough that it can remain available in production and be enabled only when needed.

Getting Started

Practical Runbooks

Architecture

Output & Formatting

Backends

Runtime Control

Tools

Reference

Examples

Clone this wiki locally