Skip to content

Add _lite variants that skip backtrace capture#454

Closed
FuuuOverclocking wants to merge 2 commits into
dtolnay:masterfrom
FuuuOverclocking:fuu/add-lite
Closed

Add _lite variants that skip backtrace capture#454
FuuuOverclocking wants to merge 2 commits into
dtolnay:masterfrom
FuuuOverclocking:fuu/add-lite

Conversation

@FuuuOverclocking

Copy link
Copy Markdown

Motivation

Backtrace::capture() incurs a fixed ~4 us overhead per anyhow::Error construction when RUST_LIB_BACKTRACE=1. For most applications this is acceptable, but on hot paths where errors are constructed at high frequency, it becomes a bottleneck. Today the only escape hatch is globally disabling backtraces via environment variable, which sacrifices debuggability everywhere.

Solution

Introduce _lite variants that unconditionally skip backtrace capture:

Standard API Lite variant
Error::new(e) Error::new_lite(e)
Error::msg(m) Error::msg_lite(m)
anyhow!(...) anyhow_lite!(...)
.context(c) .context_lite(c)
.with_context(f) .with_context_lite(f)

Plus Error::backtraced() to retroactively capture a backtrace at a cold boundary, upgrading a lite error without losing the trace entirely.

All variants retain identical error-chaining and downcast semantics.

Benchmark Results (Intel Xeon 6982P-C, pinned core)

Benchmark BT=0 BT=1 (DWARF) BT=1 (frame ptr)
thiserror construct 0 ns 0 ns 0 ns
anyhow_lite!() 15 ns 15 ns 15 ns
anyhow!() 16 ns 4,209 ns 4,147 ns
.context_lite() 15 ns 15 ns 16 ns
.context() 17 ns 4,242 ns 4,177 ns

Stack depth scaling (BT=1 DWARF):

Depth anyhow!() anyhow_lite!()
8 4,223 ns 16 ns
32 4,228 ns 16 ns
128 4,224 ns 16 ns

The lite variants are ~260x faster, constant at ~15 ns regardless of backtrace settings or stack depth.

Usage

// Hot path: no backtrace overhead
fn hot_loop(items: &[Item]) -> anyhow::Result<()> {
    for item in items {
        parse(item).context_lite("parse failed")?;
    }
    Ok(())
}

// Cold boundary: upgrade with backtrace if needed
pub fn api_entry(items: &[Item]) -> anyhow::Result<()> {
    hot_loop(items).map_err(|e| e.backtraced())
}

FAQ: Why not use thiserror on the hot path from the start?

  1. Write correct code first, optimize later. anyhow minimizes boilerplate during early development; optimization should be driven by profiling, not premature decisions.
  2. Large codebases make refactoring hard. Converting a deep call chain from anyhow::Result to a custom enum touches every signature and ? site. The _lite suffix is a one-token change at the call site with the same performance benefit.

Design Notes

  • All _lite methods pass None for the backtrace parameter to existing internal constructors — no new unsafe code, no architectural changes.
  • backtraced() is idempotent: if a backtrace already exists, it's a no-op.
  • #[cfg(feature = "std")] gates all backtrace-related additions; no-std builds are unaffected.

Provide zero-backtrace-cost alternatives for performance-critical paths:

- Error::new_lite() / Error::msg_lite()
- .context_lite() / .with_context_lite()
- anyhow_lite!() macro
- Error::backtraced() to retroactively capture at a cold boundary

These pass None as the backtrace parameter unconditionally, avoiding
the ~4us fixed overhead of Backtrace::capture() while retaining the
same error-chaining semantics.

The backtraced() method allows "upgrading" a lite error with a
backtrace at the point where it leaves the hot path, so the trace
starts from the cold boundary rather than being lost entirely.

Intended usage: keep the lightweight error type inside hot loops and
convert to full anyhow::Error (with backtrace) at cold API boundaries.

Signed-off-by: Fuu <[email protected]>
Introduce benches/error_cost.rs measuring the cost tiers:

  thiserror construct:        ~0 ns  (optimized away entirely)
  anyhow_lite!/context_lite: ~15 ns  (heap alloc, no backtrace)
  anyhow!/context (BT=1):  ~4.2 us  (heap alloc + backtrace capture)

Also includes stack-depth scaling tests (8/32/128 frames) and a
run_bench.sh script that drives three rounds:
  BT=0, BT=1 (DWARF CFI), BT=1 + frame-pointers.

Key findings on Intel Xeon 6982P-C:
- Backtrace capture is a ~4.2us fixed cost, independent of stack depth
  and unwind method (DWARF vs frame-pointer differ by only 1-3%)
- The _lite variants remain constant at ~15 ns regardless of
  RUST_LIB_BACKTRACE, confirming a ~260x reduction

Signed-off-by: Fuu <[email protected]>

@dtolnay dtolnay left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. I would prefer not to support this in this crate and instead encourage anything that would consider using these APIs to switch to a different error type.

@dtolnay dtolnay closed this Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants