Memory Debugging¶
Learning Objectives
Use
memcheckto find memory leaks, invalid reads/writes, and uninitialized valuesUse
helgrindto detect data races in multithreaded codeUse
cachegrindto simulate cache behavior and find cache missesUse
callgrindto find hotspots by instruction countInterpret 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 retiredI1,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