The custom memory allocator is designed to work correctly with Valgrind, the popular memory debugging and profiling tool. This document explains how to use Valgrind with the allocator and what to expect.
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./testThis will run the test suite and check for:
- Memory leaks
- Invalid memory access
- Use of uninitialized values
- Double frees
- Invalid frees
valgrind --leak-check=full ./benchmarkNote: Benchmarks will run significantly slower under Valgrind (10-50x slower). This is expected behavior.
==12345== HEAP SUMMARY:
==12345== in use at exit: 0 bytes in 0 blocks
==12345== total heap usage: X allocs, X frees, Y bytes allocated
==12345==
==12345== All heap blocks were freed -- no leaks are possible
==12345==
==12345== For lists of detected and suppressed errors, rerun with: -s
==12345== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Valgrind tracks both:
- brk-based allocations: Memory from
sbrk()system call - mmap-based allocations: Direct memory mappings for large blocks
The allocator uses both mechanisms, which Valgrind correctly identifies.
If you see "still reachable" blocks at program exit:
==12345== HEAP SUMMARY:
==12345== in use at exit: 65,536 bytes in 1 blocks
==12345== total heap usage: 100 allocs, 99 frees, 100,000 bytes allocated
==12345==
==12345== 65,536 bytes in 1 blocks are still reachable
Explanation: The heap space acquired via brk() is not automatically returned to the OS. This is normal behavior and not a memory leak. The memory is managed by the allocator's free lists.
Solution: This is expected. To verify it's not a leak, ensure all user-level mem_malloc() calls have corresponding mem_free() calls.
Large allocations (≥128KB) use mmap() directly:
==12345== HEAP SUMMARY:
==12345== in use at exit: 0 bytes in 0 blocks
==12345== total heap usage: 50 allocs, 50 frees, 500,000 bytes allocated
These are properly tracked by Valgrind and will show as freed when mem_free() is called.
If Valgrind reports invalid memory access:
==12345== Invalid write of size 4
==12345== at 0x4E89BD7: (your code)
==12345== Address 0x... is 4 bytes after a block of size 100 alloc'd
Cause: Buffer overflow - writing beyond allocated memory Solution: Check array bounds and allocated sizes
==12345== Invalid read of size 4
==12345== at 0x4E89BD7: (your code)
==12345== Address 0x... is 0 bytes inside a block of size 100 free'd
Cause: Using memory after calling mem_free()
Solution: Don't access pointers after freeing them
==12345== Invalid free() / delete / delete[] / realloc()
==12345== at 0x4C2EDEB: mem_free (allocator.c:...)
Cause: Calling mem_free() twice on the same pointer
Solution: Set pointers to NULL after freeing
Run Valgrind frequently during development:
make clean
make test
valgrind --leak-check=full ./testIf the allocator's internal state shows as "still reachable" (which is normal), create a suppression file:
{
allocator_heap_residual
Memcheck:Leak
match-leak-kinds: reachable
fun:sbrk
...
}
Use it with:
valgrind --suppressions=allocator.supp --leak-check=full ./testFor uninitialized value errors, use --track-origins=yes:
valgrind --track-origins=yes ./testThis helps identify where uninitialized values come from.
For detailed information:
valgrind -v --leak-check=full --show-leak-kinds=all ./testValgrind classifies leaks as:
-
Definitely lost: Memory you allocated but no longer have pointers to
- Action: Fix these! These are real leaks.
-
Indirectly lost: Memory pointed to by definitely lost blocks
- Action: Fix the "definitely lost" blocks first.
-
Possibly lost: Pointers to memory exist but may not be valid
- Action: Investigate these.
-
Still reachable: Memory that could still be accessed at program exit
- Note: For the allocator's heap space, this is normal.
total heap usage: 1,000 allocs, 1,000 frees, 256,000 bytes allocated
- allocs: Total calls to allocation functions
- frees: Total calls to free functions
- bytes allocated: Total bytes requested (not including overhead)
If allocs == frees and no "definitely lost" blocks exist, your program is clean.
Use this checklist when testing with Valgrind:
- No "definitely lost" blocks
- No "indirectly lost" blocks
- No invalid reads or writes
- No use of uninitialized values
- No double frees
- Same number of allocations and frees
- "Still reachable" blocks are expected (heap space)
Valgrind adds significant overhead:
| Test Type | Normal Speed | Valgrind Speed | Slowdown |
|---|---|---|---|
| Unit tests | <1 second | 2-5 seconds | 5-10x |
| Benchmarks | 0.02 seconds | 1-2 seconds | 50-100x |
Note: Never use Valgrind results to measure performance. Use it only for correctness checking.
Example GitHub Actions workflow:
name: Valgrind Check
on: [push, pull_request]
jobs:
valgrind:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Valgrind
run: sudo apt-get install -y valgrind
- name: Build
run: make all
- name: Run Valgrind
run: make valgrindCause: Using memory before initializing it
Solution: Initialize all variables, or use mem_calloc() instead of mem_malloc()
Cause: Mixing system malloc with custom allocator
Solution: Don't mix malloc() with mem_free() or vice versa
Cause: Overlapping memory regions in memcpy()
Solution: Use memmove() instead of memcpy() for overlapping regions
- Valgrind Quick Start: https://valgrind.org/docs/manual/quick-start.html
- Valgrind Manual: https://valgrind.org/docs/manual/manual.html
- Memcheck Documentation: https://valgrind.org/docs/manual/mc-manual.html
The custom memory allocator is designed to be Valgrind-friendly. Regular Valgrind testing ensures memory safety and helps catch bugs early in development. "Still reachable" heap blocks are expected and normal for this allocator implementation.