1

I have come across some unexplained memory behavior on Linux in my imaging application. Specifically, it appears that the application holds onto a certain amount of resident memory and will never release it. In our application, it appears that when allocating larger chunks, such as four sessions that each use 3GBs, once all resources are released the baseline may jump 1GB and then stay constant for a while. At some later point, it might jump again. The exact same sequence of operations on Windows demonstrates the expected memory behavior where the baseline remains fixed i.e. memory usage comes back to what it was when the application started.

We have profiled the code extensively to confirm the same number of allocations / deletions, verified in valgrind and that has shown no leaks.

When the image buffer is a simple array of char, allocation and deallocation behave as expected. If the image buffer is a class which contains the array of char, that too works as expected. If I have a container class (DicomImageData) that hold the image buffer class, the problem occurs. It has been isolated and shown below.

First run with an input parameter (WORK_AROUND_LEAK in C++ code below) set to 0. The first run will allocate 2.9GBs of memory and then release it, as viewed by top. The second and all subsequent runs, it will allocate 2.9GBs of memory, but it will not be released.

Work around

I discovered a “work around” by pure luck, but it appears that if I create an intermediate variable to hold the vector of DicomImageData objects, this issue no longer occurs. To verify, run it again with the input parameter set to 1 and do the same sequence. Memory will continually grow to 2.3GB, and then be released. I have tested on various Ubuntu and RHEL implementations and all behave in a consistent manner.

For reference, the dev system is Ubuntu 18.04.4 LTS with 72 cores and 32GB of memory, however running with 8-cores and 16GB exhibits identical behavior, although the 16GB system will thrash much sooner. I’m using gcc 7.5.0.

Any help would be appreciated.

My cpp code and Makefile are as below C++ source code


#define WORK_AROUND_LEAK 0

#include <stdio.h>
#include <iostream>
#include <vector>
#include <string.h>

int totalImages = 2299;

// Class to hold image data
class PixelData {
public:
    char * buffer;
    size_t size;

    PixelData()
        : buffer(NULL)
        , size(0) {
    }

    ~PixelData() {
         if (buffer) {
             delete[] buffer;
             buffer = NULL;
         }
        size = 0;
    }
};

// Class to hold above defined Pixel data and some other meta-data relevant to the image
class DicomImageData {
public:
    PixelData * pixelData;

    DicomImageData() {
        pixelData = NULL;
    }

    ~DicomImageData() {
        if (pixelData) {
             delete pixelData;
             pixelData = NULL;
        }
    }
};


using namespace std;

int main() {

#if WORK_AROUND_LEAK
    std::vector<DicomImageData *> imageDataArray[1];
#endif
    char c = 'c';
    int executionCount(0);
    size_t pixelDataSize(2 * 1024 * 520);
    do {
        int numIterations = 1;
        executionCount++;
        cout << "\nStarting execution test1 " << executionCount << endl;

        int iter = 0;
        cout << "Starting execution test iterations " << iter << endl;

        std::vector<DicomImageData*> imageData;

        for (size_t i = 0; i < totalImages; i++) {

            char * readPixelData = new char [pixelDataSize + 1 + (i*3)];
            memset(readPixelData, '@', pixelDataSize + 1);

            DicomImageData * dicomImageData = new DicomImageData();
            if (dicomImageData) {
                // Create new pixel data struct and set
                PixelData * pixelData = new PixelData();
                pixelData->buffer = readPixelData;
                pixelData->size = pixelDataSize;
                dicomImageData->pixelData = pixelData;

                imageData.push_back(dicomImageData);
            }
#if WORK_AROUND_LEAK
        imageDataArray[0] = imageData;
#endif
        }
        printf("\nPress ENTER to release memory \n");
        scanf("%c", &c);


        iter = 0;
        cout << "Starting release iterations " << iter << endl;
#if WORK_AROUND_LEAK
        imageData = imageDataArray[0];
#endif
        for (size_t i = 0; i < totalImages; i++) {

            PixelData *pixelData = imageData[i]->pixelData;
            delete[] pixelData->buffer;
            pixelData->buffer = NULL;

            delete imageData[i]->pixelData;
            imageData[i]->pixelData = NULL;
            
            delete imageData[i];
            imageData[i] = NULL;
        }

        imageData.clear();

        printf("Press ENTER to run another test or press X to exit \n");
        scanf("%c", &c);
    } while (c != 'x' && c != 'X');
    
}

Makefile if needed

all: memory_leak

memory_leak: 
    g++ -std=c++11 -O2 -D NDEBUG memory_leak.cpp -o memory_leak

clean: 
    rm memory_leak
Abhijit
  • 11
  • 2
  • 2
    "Specifically, it appears that the application holds onto a certain amount of resident memory and will never release it." Correct. Just because you `delete` something, it doesn't mean that the memory will be guaranteed to be released to the operating system. C++ does not work this way. The library simply recycles this pool of memory for whatever the program might `new` in the future. This is not a memory leak. – Sam Varshavchik Oct 13 '20 at 02:22
  • 1
    Dynamic memory management is often expensive (e.g. requests to the operating system to allocate memory, which in turn allocates hardware resources). So quite a few implementations of the C or C++ standard library optimise memory usage - when the program releases memory, that memory is marked for reuse, and not returned immediately to the OS. A subsequent request by the program for memory then is assigned previously used memory, rather than requesting it from the OS. The OS may similarly manage hardware resources. These effects can mean the program holds on to memory for a while. – Peter Oct 13 '20 at 02:26
  • If you want more control of the memory allocations and deallocations, you may want to bypass the C++ standard library allocator and request the memory directly from the operating system's kernel, using the functions [mmap](https://man7.org/linux/man-pages/man2/mmap.2.html) on Linux and [VirtualAlloc](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc) on Windows. That way, you can explicitly return this memory to the kernel using the corresponding functions listed in the documentation of these functions. – Andreas Wenzel Oct 13 '20 at 02:33
  • 1
    [This question](https://stackoverflow.com/questions/4779188/how-to-use-mmap-to-allocate-a-memory-in-heap) might be useful in understanding how to use `mmap` to allocate memory directly from the kernel. You can return it directly to the kernel using `munmap`. – Andreas Wenzel Oct 13 '20 at 02:44
  • Looks like the code actually works. **BUT** this is poorly written C++ with a couple of issues. Take if for a code review https://codereview.stackexchange.com/ – Martin York Oct 13 '20 at 05:40
  • @Peter In cases the library is going to hold onto the memory, would you not expect it to be freed if system is running low on memory? Just like managed languages, is there a way to run something akin to garbage collector to force release the memory? – Abhijit Oct 13 '20 at 21:39
  • @SamVarshavchik probably the same question for you as the suggestions is similar Peter's suggestion – Abhijit Oct 13 '20 at 21:40
  • @MartinYork I understand and this is not production code. I was just trying to reproduce the relevant part of the code to explain my problem. Do you think I can get suggestion after code review that will help fix this issue? – Abhijit Oct 13 '20 at 21:42
  • @AndreasWenzel I will try out your suggestion. If the standard library does not release memory even when system is running low on memory, something like a force release would be a nice feature to have. Short of that, seems like my option is to use mmap and VirtualAlloc as you suggested and if it works for us. – Abhijit Oct 13 '20 at 21:43
  • I don't know what question you're referring to, but both of us pretty much stated the same thing. I did not suggest anything, only described how the C/C++ library works, and has always worked for many decades. You cannot change this behaviour. – Sam Varshavchik Oct 13 '20 at 21:57
  • @Abhijit - C++ is not a managed language, and no garbage collection is included. As such, there is no requirement that the standard library monitor system resource usage to determine if/when they release memory - such monitoring involves a non-trivial overhead and complicated coding logic, for a feature most C++ developers don't need (since they do development that either doesn't benefit from such a feature, or is adversely impacted by it) so no implementations do that. If you need a managed language with garbage collection built in, you need another language. – Peter Oct 14 '20 at 00:38
  • @Abhijit The code review site is for working code only. So you need to fix the issue first. I would still get a code review. It is not likely to hurt. – Martin York Oct 14 '20 at 00:52

1 Answers1

0

To my knowledge C++ does not initialize variables to a null value so

    ~DicomImageData() {
        if (pixelData) {
             delete pixelData;
             pixelData = NULL;
        }
    }

might be the problem. Try this

// Class to hold image data
class PixelData {
public:
    char * buffer = nullptr;
    size_t size;

    PixelData()
        : buffer(nullptr)
        , size(0) {
    }

    ~PixelData() {
         if (buffer!=nullptr) {
             delete[] buffer;
             buffer = nullptr;
         }
        size = 0;
    }
};

Do the same nullptr initialization for the other class that has a pointer member variable. Also, you can avoid this entirely by using a unique_ptr to hold your pointers. See this post on how to use a unique_ptr to hold an array. Always try to use a smart pointers until you can't.

  • Thanks @Jide. I tried your suggestion but it did not work. Specifically the behavior of the application remains the same as described in the problem i.e. it locks the memory and does not release it back to the OS. – Abhijit Oct 13 '20 at 21:45
  • The question has nothing to do, whatsoever, with initialization to null values. – Sam Varshavchik Oct 13 '20 at 21:56
  • @Abhijit darn try using smart_ptrs instead of raw ptrs for the member variables. – Jidè Oct 13 '20 at 22:09