Which Profiler Should I Use?
Choose the right C++ profiler — Tracy vs perf vs Valgrind Callgrind vs gprof vs VTune, based on platform, workload type, and what you're measuring.
Quick answer
| I need to… | Use |
|---|---|
| Profile a game / real-time app with frame data | Tracy |
| Profile on Linux, minimal overhead, production-safe | perf |
| Find exact instruction counts and cache misses | Valgrind Callgrind |
| Quick baseline, no setup, works everywhere | gprof |
| Deep CPU microarchitecture analysis | Intel VTune |
| Profile on macOS | Instruments |
| Find memory leaks / heap allocation hotspots | Valgrind Massif or Heaptrack |
| Profile GPU / CUDA code | NVIDIA Nsight |
Decision flow
What are you profiling?
├── Memory (leaks, allocations, heap usage)
│ ├── Linux → Heaptrack (low overhead) or Valgrind Massif (exact)
│ └── Any → AddressSanitizer for leaks (-fsanitize=address)
│
├── GPU / CUDA / OpenCL
│ ├── NVIDIA → Nsight Systems / Nsight Compute
│ └── AMD → ROCm profiler / RGP
│
└── CPU performance
├── Real-time / game / frame-level visibility?
│ └── YES → Tracy (instrumented, very low overhead)
│
├── Which platform?
│ ├── Linux → perf (sampling, no recompile needed)
│ ├── macOS → Instruments (Xcode)
│ └── Windows → VTune or Visual Studio Diagnostics
│
├── Need exact instruction counts?
│ └── YES → Valgrind Callgrind (slow but precise)
│
└── Need CPU microarch details (pipeline, memory stalls)?
└── YES → Intel VTune or AMD uProfTracy
Best for: games, simulations, any app where frame timing matters.
- Instrumented: add
ZoneScopedmacros to functions of interest - Real-time visualization GUI — see frame timeline, zone durations, memory
- Very low overhead (~50ns per zone), suitable for release builds
- Supports locks, memory tracking, GPU zones, fiber switches
- Requires recompile with
TRACY_ENABLE
#include <tracy/Tracy.hpp>
void update(float dt) {
ZoneScoped; // marks this function in Tracy
// ...
FrameMark; // end of frame
}Limitation: needs instrumentation; blind spots where you forgot to add zones.
Linux perf
Best for: Linux production systems, no-recompile profiling.
- Statistical: samples program counter at ~1000 Hz — no instrumentation
- Hardware counters: cache misses, branch mispredictions, IPC
- Low overhead — suitable for production
- Flamegraph generation with Brendan Gregg's scripts
perf annotateshows which source lines/instructions are hot
perf record -g -F 99 ./myapp
perf script | flamegraph.pl > out.svgLimitation: Linux only; sampling means rare codepaths may be missed; needs debug symbols for readable reports.
Valgrind Callgrind
Best for: when you need exact instruction counts, not statistical estimates.
- Simulates the CPU — every instruction counted exactly
- Cache simulation: exact L1/L2/LLC miss counts per function/line
- No sampling noise — even rarely-called functions appear correctly
- KCachegrind GUI for visualization
Limitation: 20–100× slowdown — not suitable for profiling with realistic workloads; time-sensitive code behaves differently.
gprof
Best for: quick baseline when you just want to know which functions dominate.
- Requires
-pgcompile flag — no external tool needed - Works on GCC/Clang, any platform
- Low overhead, simple flat profile output
Limitations: doesn't work well with shared libraries or inlined functions; superseded by perf for most use cases; no flamegraph support out of the box.
Intel VTune
Best for: CPU microarchitecture bottleneck analysis.
- Memory access patterns, cache thrashing, TLB misses
- Pipeline analysis — frontend/backend bound, branch mispredictions
- Threading analysis — lock contention, wait times
- Works on any CPU (not just Intel) for basic analysis
Limitation: commercial (free for open source); complex UI; overkill unless you're squeezing the last 5%.
Instruments (macOS)
Best for: any macOS development — native, well-integrated.
- Built into Xcode, no setup
- Time Profiler: sampling CPU profiler
- Allocations: heap profiling
- Leaks: leak detection
- System Trace: see thread scheduling and syscalls
Heaptrack
Best for: finding allocation hotspots on Linux with low overhead.
- Intercepts malloc/free/new/delete
- Much faster than Valgrind Massif (~5× slowdown vs 20–100×)
- GUI (heaptrack_gui) shows allocation call stacks, heap growth over time
heaptrack ./myapp
heaptrack_gui heaptrack.myapp.*.gzProfiling workflow
- Reproduce the problem — with a realistic, repeatable workload
- Profile release build — with debug info (
-O2 -gorRelWithDebInfo) - Identify top hotspot — highest self-time function
- Understand why it's slow — CPU bound? Cache bound? Lock contention?
- Optimize one thing — change, re-build, re-profile
- Verify improvement — always measure after each change
- Repeat until target performance is achieved
Never optimize without profiling first — intuition about hotspots is usually wrong.
perf flamegraph — full workflow
Flamegraphs are the most effective CPU profiling visualization: the x-axis is time (wider = more time), the y-axis is stack depth. The widest frames at the top are your hotspots.
# 1. Build with frame pointers (required for accurate stack unwinding)
g++ -O2 -g -fno-omit-frame-pointer -o myapp main.cpp
# 2. Record at 99 Hz for 30 seconds
perf record -F 99 -g ./myapp
# 3. Generate flamegraph (Brendan Gregg's scripts)
git clone --depth 1 https://github.com/brendangregg/FlameGraph
perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > flame.svg
# 4. Open in browser
firefox flame.svg
# Alternatively: perf top (real-time, no recording needed)
perf top -g --call-graph dwarfUseful perf one-liners:
# Count cache misses, branch mispredictions, IPC
perf stat -e cycles,instructions,cache-misses,branch-misses ./myapp
# Annotate hot source lines (needs -g build)
perf record -g ./myapp && perf annotate --stdio
# Record only one function
perf record -e cycles:u --call-graph lbr -- ./myappgoogle-benchmark — measure before you optimize
Use google-benchmark to create reproducible microbenchmarks that survive across refactors:
#include <benchmark/benchmark.h>
#include <vector>
#include <algorithm>
#include <numeric>
// Benchmark a function with varying input size
static void BM_Sort(benchmark::State& state) {
std::vector<int> data(state.range(0));
std::iota(data.begin(), data.end(), 0);
std::shuffle(data.begin(), data.end(), std::mt19937{42});
for (auto _ : state) {
auto copy = data;
std::sort(copy.begin(), copy.end());
benchmark::DoNotOptimize(copy.data());
benchmark::ClobberMemory();
}
state.SetComplexityN(state.range(0));
state.SetItemsProcessed(state.iterations() * state.range(0));
}
BENCHMARK(BM_Sort)->RangeMultiplier(4)->Range(256, 1 << 20)->Complexity();
// Compare two implementations
static void BM_FindLinear(benchmark::State& state) {
std::vector<int> v(10000);
std::iota(v.begin(), v.end(), 0);
for (auto _ : state)
benchmark::DoNotOptimize(std::find(v.begin(), v.end(), 9999));
}
static void BM_FindBinary(benchmark::State& state) {
std::vector<int> v(10000);
std::iota(v.begin(), v.end(), 0);
for (auto _ : state)
benchmark::DoNotOptimize(std::lower_bound(v.begin(), v.end(), 9999));
}
BENCHMARK(BM_FindLinear);
BENCHMARK(BM_FindBinary);
BENCHMARK_MAIN();# CMakeLists.txt
find_package(benchmark REQUIRED)
# Or via FetchContent:
FetchContent_Declare(gbench
GIT_REPOSITORY https://github.com/google/benchmark.git
GIT_TAG v1.8.4)
set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(gbench)
add_executable(perf_tests bench/sort_bench.cpp)
target_link_libraries(perf_tests PRIVATE benchmark::benchmark_main)# Run all benchmarks
./perf_tests
# Filter by name
./perf_tests --benchmark_filter=BM_Sort
# Run each 5 times, report mean/median/stddev
./perf_tests --benchmark_repetitions=5 --benchmark_report_aggregates_only=true
# JSON output for trend tracking
./perf_tests --benchmark_format=json > bench_$(git rev-parse --short HEAD).json
# Compare two runs (install benchmark-tools)
compare.py benchmarks baseline.json current.jsonKey rules:
- Always
DoNotOptimizethe result to prevent dead-code elimination ClobberMemory()after writes to prevent the compiler from caching stale values- Build in Release mode (
-O2/RelWithDebInfo) — never benchmark Debug builds - Run on an isolated machine or in a dedicated CI job to reduce variance
Heaptrack — allocation profiling
When you need to find which callsite allocates the most memory:
# Install
apt install heaptrack
# Profile
heaptrack ./myapp
# Analyze — GUI or CLI
heaptrack_gui heaptrack.myapp.*.gz
# CLI summary
heaptrack_print heaptrack.myapp.*.gz | head -40Heaptrack shows: total allocations, peak heap usage, leaked bytes, and the call stacks responsible. Typical findings: unnecessary std::vector copies in hot loops, repeated std::string construction, missing reserve() before push_back.