Memory Debugging

Learning Objectives

  • Use memcheck to find memory leaks, invalid reads/writes, and uninitialized values

  • Use helgrind to detect data races in multithreaded code

  • Use cachegrind to simulate cache behavior and find cache misses

  • Use callgrind to find hotspots by instruction count

  • Interpret Valgrind output and fix the underlying bugs


Why Valgrind?

Valgrind runs your program on a synthetic CPU — slow, but it sees everything. It’s invaluable for:

  • Memory bugs: leaks, buffer overruns, use-after-free

  • Threading bugs: data races, deadlocks

  • Performance: cache misses, hotspots

Valgrind has enough depth to warrant its own topic.


Memcheck — Memory Errors

The most commonly used tool:

valgrind --tool=memcheck --leak-check=full ./program

Common Errors

Invalid read/write:

Invalid read of size 8
   at 0x4E56789: your_function (your_file.c:42)
  Address 0x52043d0 is 0 bytes after a block of size 80
    alloc'd at 0x4C34567: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
    by 0x4E56123: your_function (your_file.c:40)

Memory leak:

definitely lost: 80 bytes in 1 blocks
  at 0x4C34567: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
  by 0x4E56123: your_function (your_file.c:40)

Uninitialized value:

Conditional jump or move depends on uninitialised value(s)
   at 0x4E56789: your_function (your_file.c:45)

Fixing Memory Bugs

// BUG: buffer overrun
char *buf = malloc(10);
strcpy(buf, "This string is too long!");  // writes past end

// FIX: allocate enough or use strncpy
char *buf = malloc(20);
strncpy(buf, "This string is too long!", 19);
buf[19] = '\0';

Helgrind — Data Races

Detects threading bugs in POSIX threads and OpenMP:

valgrind --tool=helgrind ./program

Example: Data Race

Thread 2:
  Write by thread 2 at 0x52043d0
    at 0x4E56789: counter++ (races.c:15)
  Previous write by thread 1 at 0x52043d0
    at 0x4E56789: counter++ (races.c:15)

Fixing Data Races

// BUG: data race on shared counter
int counter = 0;
#pragma omp parallel for
for (int i = 0; i < 1000; i++) {
  counter++;  // race!
}

// FIX: use reduction
#pragma omp parallel for reduction(+:counter)
for (int i = 0; i < 1000; i++) {
  counter++;
}

Cachegrind — Cache Simulation

Simulates cache behavior (no hardware counters needed):

valgrind --tool=cachegrind ./program
cachegrind_annotate cg.out

Output Example

Ir         I1         L2i        IL1i       L2d        IL1d       L1d
1000000    1000       500        200        5000       2000       1000
  • Ir: Instructions retired

  • I1, L2i, IL1i: L1 instruction cache, L2 instruction cache, etc.

  • L2d, IL1d, L1d: L2 data cache, L1 data cache, etc.

High miss rates indicate cache-unfriendly access patterns.


Callgrind — Hotspot Profiling

Finds hotspots by instruction count:

valgrind --tool=callgrind ./program
callgrind_annotate callgrind.out.1

Output Example

events: Ir
450000  (45.0%)  your_function (your_file.c:42)
300000  (30.0%)  another_function (another_file.c:15)

The top functions are your hotspots — optimize these first.


Running on ARC Clusters

Warning

Valgrind is slow (10–50× overhead). Do not run it on compute nodes or with large data.

Use login nodes or workstations:

# On login node (check policy first)
valgrind --tool=memcheck ./program

# Or on a workstation
ssh workstation
valgrind --tool=helgrind ./parallel_program

References