Skip to main content

Valgrind

Dynamic analysis tool suite for memory debugging, leak detection, and profiling. More thorough than sanitizers but much slower (10-50x).

When to Use Valgrind

Sanitizers can't find it? Try Valgrind. Better leak detection, more thorough checks. Trade-off: 10-50x slower.

Valgrind Tools

ToolPurposeUsage
MemcheckMemory errors, leaksDefault, most common
CachegrindCache profilingPerformance analysis
CallgrindCall graph profilingFunction call costs
HelgrindThread errorsRace detection
MassifHeap profilingMemory usage over time

Memcheck (Memory Errors)

# Basic usage
valgrind ./program

# With leak checking
valgrind --leak-check=full ./program

# Show leak details
valgrind --leak-check=full --show-leak-kinds=all ./program

# Track origins of uninitialized values
valgrind --track-origins=yes ./program

# Verbose output
valgrind -v --leak-check=full ./program

What Memcheck Detects

// 1. Invalid read/write
int arr[10];
int x = arr[10]; // Invalid read of size 4

// 2. Use of uninitialized value
int x;
if (x == 5) {} // Conditional jump depends on uninitialized value

// 3. Invalid free
int* p = new int;
delete p;
delete p; // Invalid free() / delete

// 4. Memory leak
void leak() {
int* p = new int[100];
// No delete[] // 400 bytes in 1 blocks definitely lost
}

// 5. Mismatched free/delete
int* p = new int[10];
delete p; // Mismatched free() / delete / delete[]

Valgrind Output

==12345== Invalid write of size 4
==12345== at 0x400567: main (program.cpp:10)
==12345== Address 0x5203050 is 0 bytes after a block of size 40 alloc'd
==12345== at 0x4C2E0EF: operator new[](unsigned long)
==12345== by 0x400557: main (program.cpp:8)

==12345== LEAK SUMMARY:
==12345== definitely lost: 400 bytes in 1 blocks
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 0 bytes in 0 blocks
==12345== still reachable: 0 bytes in 0 blocks
==12345== suppressed: 0 bytes in 0 blocks

Leak Types

TypeMeaning
Definitely lostMemory leak, no pointers to it
Indirectly lostLost via definitely lost block
Possibly lostPointer to middle of block
Still reachablePointer exists but not freed
SuppressedIgnored via suppressions file

Cachegrind (Cache Profiling)

Simulates CPU cache behavior.

# Run with cachegrind
valgrind --tool=cachegrind ./program

# Produces cachegrind.out.<pid>
# View with:
cg_annotate cachegrind.out.12345

# Show top functions
cg_annotate --auto=yes cachegrind.out.12345

Output shows:

  • Instruction cache misses
  • Data cache misses
  • Branch mispredictions

Callgrind (Call Graph Profiling)

Records call graph and call counts.

# Run with callgrind
valgrind --tool=callgrind ./program

# Produces callgrind.out.<pid>
# Visualize with KCachegrind (GUI)
kcachegrind callgrind.out.12345

# Or text view
callgrind_annotate callgrind.out.12345

Shows:

  • Function call counts
  • Inclusive/exclusive costs
  • Call relationships

Helgrind (Thread Errors)

Detects thread synchronization errors.

# Run with helgrind
valgrind --tool=helgrind ./program
#include <thread>

int shared = 0; // Not protected

void thread1() { shared = 1; } // Possible data race
void thread2() { shared = 2; } // Possible data race

int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
}

Helgrind reports:

==12345== Possible data race during read of size 4 at 0x601060 by thread #1
==12345== This conflicts with a previous write of size 4 by thread #2

Massif (Heap Profiling)

Tracks heap memory usage over time.

# Run with massif
valgrind --tool=massif ./program

# Produces massif.out.<pid>
# View with:
ms_print massif.out.12345

# Or GUI
massif-visualizer massif.out.12345

Shows:

  • Heap usage over time
  • Peak memory usage
  • Where allocations happen

Common Options

# Detailed leak checking
valgrind --leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
--verbose \
--log-file=valgrind.log \
./program

# Suppress known issues
valgrind --suppressions=my.supp ./program

# Generate suppressions
valgrind --gen-suppressions=all ./program

Suppression File

# my.supp
{
ignore_std_string_leak
Memcheck:Leak
...
fun:*std::string*
}

{
ignore_known_library_issue
Memcheck:Addr4
obj:/usr/lib/libfoo.so
}
# Use suppression file
valgrind --suppressions=my.supp ./program

Performance Tips

# Faster (less accurate)
valgrind --expensive-definedness-checks=no ./program

# Even faster
valgrind --partial-loads-ok=yes ./program

# Limit trace depth
valgrind --num-callers=12 ./program # Default: 12

# For large programs
valgrind --time-stamp=yes ./program

Debugging with Valgrind

# Start program paused
valgrind --vgdb=yes --vgdb-error=0 ./program

# In another terminal, attach GDB
gdb ./program
(gdb) target remote | vgdb
(gdb) continue

Common False Positives

// Uninitialized value (intentional)
std::vector<int> v(100); // Valgrind: uninitialized
// Values are uninitialized but that's OK here

// Still reachable (not a leak)
static std::string global = "hello";
// Valgrind: still reachable at exit
// Not freed, but not a leak (program ending)

Valgrind vs Sanitizers

FeatureValgrindSanitizers
Speed10-50x slower2-3x slower
CoverageMore thoroughFast enough for CI
Leak detectionExcellentGood (ASan)
Uninitialized readsYesMSan only
Thread errorsHelgrindTSan (better)
SetupNo recompileNeeds recompile

When to Use What

# Daily development: Sanitizers
g++ -fsanitize=address,undefined program.cpp

# Can't reproduce with sanitizers: Valgrind
valgrind --leak-check=full ./program

# Performance profiling: Cachegrind/Callgrind
valgrind --tool=callgrind ./program

# Thread debugging: TSan first, then Helgrind
g++ -fsanitize=thread program.cpp # Try first
valgrind --tool=helgrind ./program # If TSan doesn't find it

Real-World Example

// leak.cpp
#include <iostream>

void leak() {
int* p = new int[100];
// Forgot delete[]
}

int main() {
leak();
std::cout << "Done\n";
return 0;
}
# Compile
g++ -g leak.cpp -o leak

# Run with Valgrind
valgrind --leak-check=full ./leak

# Output shows:
# 400 bytes in 1 blocks are definitely lost
# at operator new[](unsigned long)
# by leak() (leak.cpp:5)

CI Integration

# Run tests with Valgrind
#!/bin/bash
valgrind --leak-check=full \
--error-exitcode=1 \
--errors-for-leak-kinds=definite,possible \
./test_suite

# Exit code 1 if leaks found → CI fails

Summary

info

Valgrind = dynamic analysis tool suite.

  • Memcheck (default): memory errors, leaks, uninitialized values.
  • Cachegrind: cache profiling.
  • Callgrind: call graph.
  • Helgrind: thread errors.
  • Massif: heap profiling.

  • Much slower than sanitizers (10-50x) but more thorough.
  • Use --leak-check=full for leak detection.
  • Good for finding bugs sanitizers miss.
  • Not for production (too slow).
# Most common usage:
valgrind --leak-check=full --show-leak-kinds=all ./program

# Profile:
valgrind --tool=callgrind ./program
kcachegrind callgrind.out.*

# Threads:
valgrind --tool=helgrind ./program