TRT
Sign in
Operating Systems

Profile Before Rewriting the Slow Path

Profiling attributes resource use to parts of a running program; benchmarking compares defined workloads under controlled conditions.

The Runtime Theory Team8 min read#profiling#benchmarking#measurement

Profiling attributes resource use to parts of a running program; benchmarking compares defined workloads under controlled conditions. They answer different questions — where does the time go? versus did my change help? — and using the wrong one is how teams spend a quarter optimizing a function that was never hot.

A profile is an accusation, not a verdict

A CPU profiler samples stacks on a timer (Linux perf defaults to 99 samples/second, so resolution is ~10 ms and overhead is well under 1%) and ranks functions by share of samples:

bash
$ perf record -F 99 -g -- ./server --port 8080 &
$ # generate load, then:
$ perf report --stdio | head -10
 # Overhead  Command   Shared Object  Symbol
 # 71.32%   server    server         [.] json_parse_value
 #  9.10%   server    server         [.] hashmap_get
 #  4.44%   server    libc.so.6      [.] __strlen_avx2

If 71% of samples are in JSON parsing, that is the bottleneck regardless of what the code "should" be doing — database index tuning will not move it. But a profile alone does not prove an optimization: a function can dominate because it is called 10 million times cheaply, or because each call is genuinely expensive. Pair it with counters (allocations, lock waits, cache misses) and with the call counts before deciding what to rewrite.

Sampling also has blind spots: work spent off CPU — waiting on a lock, a syscall, a disk — shows up as a gap, not as a hotspot. For those, off-CPU analysis, wall-clock profiles, or a distributed trace are the right instrument.

A benchmark is a controlled comparison

A benchmark's only job is to make two versions comparable. That requires fixing everything else:

  1. Representative input — the size and shape of production data, not a tidy 10-element array.
  2. Warm-up — JIT tiers, cache fills, connection pools and page cache all make the first run unrepresentative; discard it or run until steady state.
  3. Isolation — quiet machine, fixed CPU frequency (turbo boost off), no competing jobs, the same binary flags on both sides.
  4. Repetitions and spread — run 10+ times, report median and range. A 3% difference inside a ±15% spread is noise.
  5. The right level — a microbenchmark proves an operation got faster; only an end-to-end measurement proves the system got faster.
bash
$ hyperfine --warmup 3 './server-bench --json 64kb'
Benchmark 1: ./server-bench --json 64kb
  Time (mean ± σ):     142.6 ms ±   4.1 ms
  Range (min … max):   136.2 ms … 150.9 ms

The ±4.1 ms is the honest part: it tells you the smallest improvement you can actually claim.

Off-CPU and allocation profiles

When the CPU profile is flat but latency is bad, the time is going somewhere sampling a stack cannot see. An off-CPU profiler records where threads park — on a mutex, in read, waiting for a packet — and typically reveals the difference between "the code is slow" and "the code is waiting". An allocation profile attributes heap churn to call sites: a function allocating 50 objects per request may be cheap per object and devastating in aggregate if a garbage collector must trace them later, or if the allocator's locks are contended across cores. The three profiles answer three different questions — running, waiting, or allocating — and a serious investigation usually needs all three before it names a cause.

Where optimizations go to die

Amdahl's Law bounds everything: if 30% of runtime is the part you made 4× faster, total speedup is 1 / (0.7 + 0.3/4) ≈ 1.29×, not 4×. If the "slow path" is only 5% of the profile, the entire rewrite buys 1.03×. And local wins can be system losses: an inlined parser that eliminates a call but doubles register pressure, or a cache that speeds reads while tripling allocations and triggering GC pauses, will show up as no change or worse at the endpoint.

The discipline is the same in both tools: state the hypothesis ("most samples are in X", "this change reduces p99 by 10%"), gather evidence at the level where the cost occurs, and re-measure end to end.

Key takeaways

  • Profiles answer "where"; benchmarks answer "whether" — do not use a benchmark to find a hotspot or a profile to claim a win.
  • Sampling profiles are cheap (~1% overhead, ~10 ms resolution) and usually indict an unexpected function; watch for off-CPU time that sampling cannot see.
  • Fix input, warm-up, machine state and concurrency, repeat enough times to see variance, and report the spread with the number.
  • Respect Amdahl's Law: check the hotspot's share before rewriting it, and confirm every local improvement on end-to-end latency.

Production cost model and latency numbers

To apply Profile Before Rewriting the Slow Path effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:

Operation / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, TCP/TLS framing, tail queueing ($L = \lambda W$)Connection pooling, deadline propagation, and p95 hedged requests

Failure modes, debugging, and operational checklist

In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on Profile Before Rewriting the Slow Path, walk through three verification questions:

  1. What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save