Appearance
Profiling and Performance Measurement
C and C++ permit low-level control, but syntax does not guarantee speed. Performance work begins with a representative workload, a defined metric, and a profiler—not a belief that fewer abstractions mean fewer instructions.
Build configuration changes the question
Benchmark optimized code. -O0 measures a debugging translation, not a release design. Keep debug symbols where the profiler can symbolize stacks:
sh
cc -std=c17 -O2 -g -DNDEBUG benchmark.c -o c-benchmark
c++ -O2 -g -DNDEBUG benchmark.cpp -o benchmarkThe C and C++ commands differ only in the selected language driver and standard mode here. Real release configurations may also select architecture, link-time optimization, runtime libraries, and visibility policy.
Record compiler, flags, target architecture, dependency versions, CPU/power state, input, warmup, and repetitions. Link-time optimization and profile-guided optimization are separate experiments with build and deployment costs.
Microbenchmarks need barriers and statistics
A compiler can eliminate work whose result is unused. Benchmark frameworks such as Google Benchmark provide techniques to keep values observable and report distributions. A hand-timed loop risks measuring clock calls, optimization artifacts, allocation setup, or noise.
Use a monotonic clock for elapsed duration. Run enough repetitions, report variance, and compare effect size rather than declaring victory from one sample.
Sampling profilers find where time goes
Sampling periodically records stacks with relatively low overhead. Platform tools include Linux perf, Apple's Instruments, Visual Studio Performance Profiler, and vendor CPU profilers. Flame graphs and call trees reveal hot paths and callers.
Instrumentation profilers add hooks for detailed call timing but perturb execution more. Allocation profilers reveal churn and retained memory; cache/hardware counters reveal misses, branches, and stalls. Choose the tool based on the hypothesis.
Complexity and layout dominate many results
Before instruction-level tuning, inspect algorithms, data volume, allocations, I/O, contention, and memory layout. A contiguous vector often beats pointer-rich structures because of locality. Reducing copying can help, but replacing clear value semantics with pervasive shared ownership may add allocation and cache costs.
Reserve capacity when growth is known. Batch work across system-call boundaries. Avoid parsing or allocating repeatedly inside a hot loop. Then measure again.
Inspect generated code selectively
Compiler Explorer and local assembly output help answer whether an abstraction inlines or vectorizes. Optimization remarks can explain missed transformations. Assembly is evidence for one compiler, flags, and target—not a portable performance contract.
Undefined behavior can make impressive benchmark results meaningless. Run tests and sanitizers in separate compatible configurations before trusting optimized measurements.
End-to-end latency includes more than CPU
Startup, dynamic loading, page faults, filesystem cache, network latency, lock contention, and scheduling can dominate. Optimize the metric users experience. A faster inner loop that increases tail latency or memory may be a regression.
Optional prompts
Explain: Why is -O0 a poor configuration for release performance comparison?
Answer: It intentionally preserves debug-friendly structure and disables transformations central to compiled performance, so it can rank designs differently from shipped code.
Explain: Why can an unused benchmark calculation appear impossibly fast?
Answer: The optimizer may prove it has no observable effect and remove it. A benchmark framework or explicit observation barrier keeps measured work present.