Skip to content
C++

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 dataTracy
Profile on Linux, minimal overhead, production-safeperf
Find exact instruction counts and cache missesValgrind Callgrind
Quick baseline, no setup, works everywheregprof
Deep CPU microarchitecture analysisIntel VTune
Profile on macOSInstruments
Find memory leaks / heap allocation hotspotsValgrind Massif or Heaptrack
Profile GPU / CUDA codeNVIDIA Nsight

Decision flow

cpp
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 uProf

Tracy

Best for: games, simulations, any app where frame timing matters.

  • Instrumented: add ZoneScoped macros 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
cpp
#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 annotate shows which source lines/instructions are hot
bash
perf record -g -F 99 ./myapp
perf script | flamegraph.pl > out.svg

Limitation: 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 -pg compile 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
bash
heaptrack ./myapp
heaptrack_gui heaptrack.myapp.*.gz

Profiling workflow

  1. Reproduce the problem — with a realistic, repeatable workload
  2. Profile release build — with debug info (-O2 -g or RelWithDebInfo)
  3. Identify top hotspot — highest self-time function
  4. Understand why it's slow — CPU bound? Cache bound? Lock contention?
  5. Optimize one thing — change, re-build, re-profile
  6. Verify improvement — always measure after each change
  7. 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.

bash
# 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 dwarf

Useful perf one-liners:

bash
# 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 -- ./myapp

google-benchmark — measure before you optimize

Use google-benchmark to create reproducible microbenchmarks that survive across refactors:

cpp
#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();
cmake
# 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)
bash
# 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.json

Key rules:

  • Always DoNotOptimize the 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:

bash
# Install
apt install heaptrack

# Profile
heaptrack ./myapp

# Analyze — GUI or CLI
heaptrack_gui heaptrack.myapp.*.gz

# CLI summary
heaptrack_print heaptrack.myapp.*.gz | head -40

Heaptrack 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.