Obtaining accurate heap usage in Windows (for debugging memory leaks in a single process)

2

I've worked for years as an embedded SW developer, in which memory leaks (even minimal amounts of memory) were often really critical.

In these environments usually it was possible to evaluate the amount of used heap, and this made possible at least a rudimentary/rough memory-leak debugging just by printing the total amount of used memory in key points of the code.

Now I'm developing a C++ parser in Windows environment and... surprisingly I cannot find a way to trace this basic information. So the question is: how can I do that?

Before answering let me say that for some reasons I'm not interested in Valgrind-like tools.

Before asking a new question I've read a lot of previous questions, such as:

How to get memory usage under Windows in C++

Which member in PROCESS_MEMORY_COUNTERS structure gives the current used memory

How to determine CPU and memory consumption from inside a process?

But none of them provided a solution suitable for my needs. So I decided to write a new question in which to make clear (1) what I exactly need and (2) the attempts I already made trying to achieve my goal.

For this reason, below I provide a minimal example program in which I perform some 128kB (0x20000 bytes) allocations (in different ways) and then I perform the corresponding memory release. After each step, I call a debugMemory() utility that prints every field of PROCESS_MEMORY_COUNTERS_EX structure:

#include <stdio.h>
#include <windows.h>
#include <psapi.h>

#define ONE_K 1024

static void debugMemory( const char * header )
{
  PROCESS_MEMORY_COUNTERS_EX pmc;

  if( header )
  {
    printf("%s:\t\tGetProcessMemoryInfo() returned %d\n", header, GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc)));
  
    printf("%s:\tPageFaultCount\t\t\t= %d 0x%08X\n",         header, pmc.PageFaultCount, pmc.PageFaultCount);
    printf("%s:\tPeakWorkingSetSize\t\t= %d 0x%08X\n",       header, pmc.PeakWorkingSetSize, pmc.PeakWorkingSetSize);
    printf("%s:\tWorkingSetSize\t\t\t= %d 0x%08X\n",         header, pmc.WorkingSetSize, pmc.WorkingSetSize);
    printf("%s:\tQuotaPeakPagedPoolUsage\t\t= %d 0x%08X\n",  header, pmc.QuotaPeakPagedPoolUsage, pmc.QuotaPeakPagedPoolUsage);
    printf("%s:\tQuotaPagedPoolUsage\t\t= %d 0x%08X\n",      header, pmc.QuotaPagedPoolUsage, pmc.QuotaPagedPoolUsage);
    printf("%s:\tQuotaPeakNonPagedPoolUsage\t= %d 0x%08X\n", header, pmc.QuotaPeakNonPagedPoolUsage, pmc.QuotaPagedPoolUsage);
    printf("%s:\tQuotaNonPagedPoolUsage\t\t= %d 0x%08X\n",   header, pmc.QuotaNonPagedPoolUsage, pmc.QuotaNonPagedPoolUsage);
    printf("%s:\tPagefileUsage\t\t\t= %d 0x%08X\n",          header, pmc.PagefileUsage, pmc.PagefileUsage);
    printf("%s:\tPeakPagefileUsage\t\t= %d 0x%08X\n",        header, pmc.PeakPagefileUsage, pmc.PeakPagefileUsage);

    printf( "%s:\tPrivateUsage\t\t\t= %d 0x%08X\n", header, pmc.PrivateUsage, pmc.PrivateUsage );
  }
}

int main(void)
{
  /* Initial */
  debugMemory("INI");
  Sleep(5000);

  /* Malloc */
  char *p1 = (char *) malloc(128 * ONE_K);
  debugMemory("MALLOC");
  Sleep(5000);

  /* New */
  char *p2 = new char[128 * ONE_K];
  debugMemory("NEW");
  Sleep(5000);

  /* Free */
  free( p1 );
  debugMemory("FREE");
  Sleep(5000);

  /* Delete */
  delete[] p2;
  debugMemory("DELETE");
  
  return 0;
}

According to most answers to SO questions, fields WorkingSetSize and PrivateUsage were the best candidates for providing the information I need. Anyway, just to provide a complete scenario, I post the results for all of them:

INI:            GetProcessMemoryInfo() returned 1
INI:    PageFaultCount                  = 766 0x000002FE
INI:    PeakWorkingSetSize              = 2834432 0x002B4000
INI:    WorkingSetSize                  = 2830336 0x002B3000
INI:    QuotaPeakPagedPoolUsage         = 22448 0x000057B0
INI:    QuotaPagedPoolUsage             = 22448 0x000057B0
INI:    QuotaPeakNonPagedPoolUsage      = 4864 0x000057B0
INI:    QuotaNonPagedPoolUsage          = 4480 0x00001180
INI:    PagefileUsage                   = 1069056 0x00105000
INI:    PeakPagefileUsage               = 1069056 0x00105000
INI:    PrivateUsage                    = 1069056 0x00105000
MALLOC:         GetProcessMemoryInfo() returned 1
MALLOC: PageFaultCount                  = 794 0x0000031A
MALLOC: PeakWorkingSetSize              = 2949120 0x002D0000
MALLOC: WorkingSetSize                  = 2945024 0x002CF000
MALLOC: QuotaPeakPagedPoolUsage         = 22448 0x000057B0
MALLOC: QuotaPagedPoolUsage             = 22448 0x000057B0
MALLOC: QuotaPeakNonPagedPoolUsage      = 4864 0x000057B0
MALLOC: QuotaNonPagedPoolUsage          = 4480 0x00001180
MALLOC: PagefileUsage                   = 1204224 0x00126000
MALLOC: PeakPagefileUsage               = 1204224 0x00126000
MALLOC: PrivateUsage                    = 1204224 0x00126000
NEW:            GetProcessMemoryInfo() returned 1
NEW:    PageFaultCount                  = 797 0x0000031D
NEW:    PeakWorkingSetSize              = 2961408 0x002D3000
NEW:    WorkingSetSize                  = 2957312 0x002D2000
NEW:    QuotaPeakPagedPoolUsage         = 22448 0x000057B0
NEW:    QuotaPagedPoolUsage             = 22448 0x000057B0
NEW:    QuotaPeakNonPagedPoolUsage      = 4864 0x000057B0
NEW:    QuotaNonPagedPoolUsage          = 4480 0x00001180
NEW:    PagefileUsage                   = 1339392 0x00147000
NEW:    PeakPagefileUsage               = 1339392 0x00147000
NEW:    PrivateUsage                    = 1339392 0x00147000
FREE:           GetProcessMemoryInfo() returned 1
FREE:   PageFaultCount                  = 797 0x0000031D
FREE:   PeakWorkingSetSize              = 2961408 0x002D3000
FREE:   WorkingSetSize                  = 2957312 0x002D2000
FREE:   QuotaPeakPagedPoolUsage         = 22448 0x000057B0
FREE:   QuotaPagedPoolUsage             = 22448 0x000057B0
FREE:   QuotaPeakNonPagedPoolUsage      = 4864 0x000057B0
FREE:   QuotaNonPagedPoolUsage          = 4480 0x00001180
FREE:   PagefileUsage                   = 1339392 0x00147000
FREE:   PeakPagefileUsage               = 1339392 0x00147000
FREE:   PrivateUsage                    = 1339392 0x00147000
DELETE:         GetProcessMemoryInfo() returned 1
DELETE: PageFaultCount                  = 797 0x0000031D
DELETE: PeakWorkingSetSize              = 2961408 0x002D3000
DELETE: WorkingSetSize                  = 2957312 0x002D2000
DELETE: QuotaPeakPagedPoolUsage         = 22448 0x000057B0
DELETE: QuotaPagedPoolUsage             = 22448 0x000057B0
DELETE: QuotaPeakNonPagedPoolUsage      = 4864 0x000057B0
DELETE: QuotaNonPagedPoolUsage          = 4480 0x00001180
DELETE: PagefileUsage                   = 1339392 0x00147000
DELETE: PeakPagefileUsage               = 1339392 0x00147000
DELETE: PrivateUsage                    = 1339392 0x00147000

Let's summarize what we can understand from these results:

  • PrivateUsage seems to be the field I'm searching for: after every allocation its value is 0x21000 bigger (instead of 0x20000. But I can forgive those 0x1000 bytes of overhead)
  • Its value ISN'T reduced after memory deallocations (!!!)
  • I would have expected that unused virtual memory was given back to the OS after a certain amount of time (that's why I tried the insertion of 5s sleeps after each step) but it seems that I was wrong
  • WorkingSetSize seems toto grow after every allocation, as well, but the amount of the grouth is inconsistent as far as I can understand

Any help would be really appreciated. I'm open both to any magic function I wasn't able to find (to obtain the accurate amount of heap usage) and to any workaround trick (for example something forcing used virtual memory shown by PrivateUsage to be updated).

c++
windows
memory
memory-leaks
asked on Stack Overflow Nov 4, 2019 by Roberto Caboni • edited Jul 28, 2020 by Roberto Caboni

1 Answer

0

PrivateUsage is a perfectly acceptable metric for what you seem to be trying to do.

The reason you get 0x21000 rather than 0x20000 is because the allocation has a header before the address returned by malloc or new (to allow that allocation to be freed properly) there is nothing already in memory to satisfy the request, and the underlying operating system call to get more memory for the process ultimately returns things in multiples of pages, so it has to round up to a multiple of the page size.

The reason the value of PrivateUsage doesn't go down during your test is likely because the buffer in question is being saved to satisfy future requests. To verify this guess you can just duplicate your sections that do new and free, so that you have a "NEW2" report and a "FREE2" report. I would expect that the output for "NEW2" is the same, with respect to PrivateUsage, as the output for "FREE".

answered on Stack Overflow Jul 28, 2020 by Tim Boddy

User contributions licensed under CC BY-SA 3.0