Echo achieves 17+ million operations per second for filtered logs and 3-4 million ops/s for active logging. With compile-time filtering, filtered logs have ZERO runtime overhead - they're completely eliminated by the compiler.
- Runtime filtered logs: 56-60 ns (~17M ops/s) - just an integer comparison
- Active logging (NullSink): 260-280 ns (~3.6M ops/s) - formatting overhead only
- Active logging (file): 350 ns (~2.9M ops/s) - includes file I/O
- Active logging (console): 650 ns (~1.5M ops/s) - includes terminal I/O
.once()overhead: +3-5 ns after first call - nearly free- Category filtering: +30-40 ns for hash lookup
- CPU: 11th Gen Intel Core i7-11800H @ 2.30GHz (16 cores)
- Compiler: GCC 15.2.0
- Optimization: Release mode with -O3
- Platform: Linux x86_64
Performance of different log operations with NullSink (no I/O overhead):
| Operation | Latency (ns) | Throughput (ops/s) | Notes |
|---|---|---|---|
| Simple string literal | 609 | 1.64M | echo::info("Hello") |
| String variable | 641 | 1.56M | echo::info(str) |
| Single integer | 621 | 1.61M | echo::info(42) |
| Multiple integers | 787 | 1.27M | echo::info(1, 2, 3) |
| Single float | 767 | 1.30M | echo::info(3.14) |
| Multiple floats | 1,234 | 811K | echo::info(1.0, 2.0, 3.0) |
| Mixed types | 918 | 1.09M | echo::info("x=", 42, " y=", 3.14) |
| Long string (100 chars) | 780 | 1.28M | Large message |
| Very long string (1000 chars) | 1,224 | 817K | Very large message |
Key Insight: Simple logs are extremely fast (~600ns). Multiple arguments add ~150-200ns per additional argument.
Performance across different log levels with NullSink:
| Level | Latency (ns) | Throughput (ops/s) | Notes |
|---|---|---|---|
echo::trace() |
56 | 17.7M | Fastest level |
echo::debug() |
65 | 15.4M | Very fast |
echo::info() |
678 | 1.48M | Standard level |
echo::warn() |
699 | 1.43M | Warning level |
echo::error() |
831 | 1.20M | Error level |
echo::critical() |
810 | 1.23M | Critical level |
echo() (no level) |
531 | 1.88M | Simple print |
Why the difference? Trace/debug are often filtered at runtime, hitting the fast path. Info/warn/error typically pass through formatting.
Performance of log level filtering at runtime:
| Scenario | Latency (ns) | Throughput (ops/s) | Speedup vs Active |
|---|---|---|---|
| Filtered Logs (level=Error) | |||
trace() filtered |
61 | 16.5M | 272× faster |
debug() filtered |
60 | 16.6M | 275× faster |
info() filtered |
60 | 16.7M | 278× faster |
warn() filtered |
59 | 16.8M | 282× faster |
| Active Logs (level=Trace) | |||
trace() passes |
275 | 3.64M | Baseline |
debug() passes |
287 | 3.48M | Baseline |
info() passes |
279 | 3.59M | Baseline |
warn() passes |
281 | 3.55M | Baseline |
error() passes |
281 | 3.56M | Baseline |
critical() passes |
301 | 3.32M | Baseline |
Key Insight: Runtime filtering is 270-280× faster than active logging. The overhead is just a single integer comparison (~60ns).
Comparison of compile-time and runtime filtering:
| Filtering Type | Latency (ns) | Throughput (ops/s) | Code Size |
|---|---|---|---|
| Compile-time filtered | 0 | ∞ | Code eliminated |
| Runtime filtered | 56-60 | 16.7-17.7M | Full code present |
| Active (passes filter) | 260-290 | 3.4-3.8M | Full code present |
Compile-time filtering example:
// With -DLOGLEVEL=Error
echo::trace("expensive", calculate()); // Completely removed - 0 ns overhead
echo::debug("expensive", calculate()); // Completely removed - 0 ns overhead
echo::error("This runs", calculate()); // Exists in binary - normal overheadPerformance of the .once() modifier for preventing log spam:
| Scenario | Latency (ns) | Throughput (ops/s) | Notes |
|---|---|---|---|
| Regular logging | 278 | 3.60M | No .once() |
.once() subsequent calls |
63 | 15.95M | Hash lookup + skip |
.once() with 100 unique locations |
66 | 15.08M | Minimal degradation |
.once() + runtime filtering |
62 | 16.22M | Combined overhead |
.once() with multiple args |
240 | 4.17M | More complex message |
Key Insight: .once() adds only 3-5ns overhead after the first call. It's essentially free for preventing log spam in loops.
Example:
for (int i = 0; i < 1000000; i++) {
echo::warn("Loop warning").once(); // Prints once, then ~60ns overhead per iteration
}Performance impact of different sink configurations:
| Sink Configuration | Latency (ns) | Throughput (ops/s) | Overhead |
|---|---|---|---|
| No sinks | 620 | 1.61M | Baseline |
| NullSink (explicit) | 272 | 3.68M | Optimized path |
| Console sink (stdout) | 654 | 1.53M | +34 ns |
| File sink (single) | 350 | 2.86M | Best I/O option |
| File sink (2 files) | 425 | 2.35M | 2× I/O |
| File sink (3 files) | 520 | 1.92M | 3× I/O |
| Console + File | 704 | 1.42M | Combined I/O |
Key Insights:
- NullSink is fastest for benchmarking or disabled logging
- File sinks are faster than console due to better buffering
- Multiple sinks scale linearly - each additional sink adds ~100-150ns
Performance of category-based filtering:
| Scenario | Latency (ns) | Throughput (ops/s) | Overhead |
|---|---|---|---|
| No categories | 264 | 3.78M | Baseline |
| Category match | 323 | 3.09M | +59 ns |
| Category no match | 360 | 2.78M | +96 ns |
| Multiple categories (match) | 329-358 | 2.78-3.03M | +65-94 ns |
| All categories disabled | 102 | 9.78M | Fast path |
Key Insight: Category filtering adds 30-40ns for hash map lookup when enabled. When all categories are disabled, it's very fast (102ns).
Example:
echo::set_category_level("network", Level::Info);
echo::category("network").info("Connected"); // +30-40ns overhead for category lookupPerformance impact of different formatting patterns:
| Pattern | Latency (ns) | Throughput (ops/s) | Complexity |
|---|---|---|---|
Simple: [%l] %m |
248 | 4.03M | Minimal |
| Standard with timestamp | 257 | 3.89M | Common |
| Complex (full info) | 260 | 3.85M | All fields |
| Very complex (with source) | 269 | 3.71M | File/line info |
| Message only | 268 | 3.73M | Just message |
| Level only | 276 | 3.62M | Just level |
| Timestamp only | 284 | 3.52M | Just timestamp |
| Multiple timestamps | 318 | 3.14M | Complex |
Key Insight: Pattern complexity has minimal impact (~10-70ns difference). Even very complex patterns are fast due to efficient formatting.
Best practice for production builds:
# CMakeLists.txt
if(CMAKE_BUILD_TYPE STREQUAL "Release")
add_compile_definitions(LOGLEVEL=Error)
endif()// With -DLOGLEVEL=Error, these are completely eliminated:
echo::trace("Debug info"); // 0 ns - code doesn't exist
echo::debug("More debug"); // 0 ns - code doesn't exist
echo::info("Information"); // 0 ns - code doesn't exist
// These still work:
echo::error("Error occurred"); // Normal overhead
echo::critical("Critical!"); // Normal overheadImpact: Filtered logs have ZERO overhead - they don't exist in the binary.
Prevent log spam with minimal overhead:
// BAD - logs 1M times (~280ns × 1M = 280ms)
for (int i = 0; i < 1000000; i++) {
echo::warn("Processing...");
}
// GOOD - logs once, then ~60ns per iteration (60ms total)
for (int i = 0; i < 1000000; i++) {
echo::warn("Processing...").once();
}Impact: 4.6× faster - saves 220ms in this example.
When you want logging infrastructure but no output:
echo::clear_sinks();
echo::add_sink(std::make_shared<echo::NullSink>());
// Now all logs are ~270ns instead of ~650ns (console)
echo::info("This is fast"); // 270ns vs 650nsImpact: 2.4× faster than console output.
File sinks are faster due to better buffering:
// Console sink: ~650ns per log
echo::add_sink(std::make_shared<echo::ConsoleSink>());
// File sink: ~350ns per log (1.86× faster)
echo::add_sink(std::make_shared<echo::FileSink>("app.log"));Impact: 1.86× faster than console output.
Without compile-time filtering:
// BAD - calculate() runs even if log is filtered at runtime
echo::set_level(Level::Error);
echo::debug("Result: ", expensive_calculate()); // calculate() still runs!
// GOOD - with compile-time filtering
#define LOGLEVEL Error
echo::debug("Result: ", expensive_calculate()); // calculate() never called!Impact: With compile-time filtering, expensive functions are never called.
echo() has no filtering:
// BAD - always prints, no filtering possible
for (int i = 0; i < 1000000; i++) {
echo("iteration"); // ~530ns × 1M = 530ms
}
// GOOD - can be filtered
for (int i = 0; i < 1000000; i++) {
echo::debug("iteration").once(); // ~60ns × 1M = 60ms (8.8× faster)
}Impact: Using echo::debug().once() is 8.8× faster and can be compile-time filtered.
- Compile-time filtered: 0 ns (code eliminated)
- Runtime filtered: 56-60 ns (~17M ops/s)
- .once() subsequent: 63 ns (~16M ops/s)
- Category disabled: 102 ns (~9.8M ops/s)
- NullSink: 272 ns (~3.7M ops/s)
- File sink: 350 ns (~2.9M ops/s)
- Console sink: 654 ns (~1.5M ops/s)
- ✅ Compile-time filtering = ZERO overhead - always use in production
- ✅ Runtime filtering is 270× faster than active logging
- ✅ .once() adds only 3-5ns after first call - use freely in loops
- ✅ File sinks are 1.86× faster than console sinks
- ✅ Category filtering adds 30-40ns - minimal overhead
- ✅ Pattern complexity barely matters - use what you need
- Always set
-DLOGLEVEL=Errorin release builds - Use
.once()in loops to prevent spam - Prefer file sinks over console for performance
- Use NullSink when logging is disabled
- Don't worry about pattern complexity - it's fast enough
Echo is designed to be one of the fastest C++ logging libraries:
- Faster than most due to compile-time filtering
- Competitive with spdlog for active logging
- Much faster when logs are filtered (270× speedup)
- Zero overhead when compile-time filtered (unique feature)
For maximum performance: Always use -DLOGLEVEL=Error in production builds!