10

Reading Martin Sustrick's blog on challenges attendant with preventing "undefined behavior" in C++, vs C, in particular the problem attendant with malloc() failing due to memory exhaustion, I was reminded of the many, many times I have been frustrated to know what to do in such cases.

With virtual systems such conditions are rare, but on embedded platforms, or where performance degradation attendant with hitting the virtual system equates to failure, as is Martin's case with ZeroMQ, I resolved to find a workable solution, and did.

I wanted to ask the readers of StackOverflow if they've tried this approach, and what their experience with it was.

The solution is to allocate a chunk of spare memory off the heap with a call to malloc() at the start of the program, and then use that pool of spare memory to stave off memory exhaustion when and if it occurs. The idea is to prevent capitulation in favor of an orderly retreat (I was reading the accounts of Kesselring's defense of Italy last night) where error messages and IP sockets and such will work long enough to (hopefully) at least tell the user what happened.

#define SPARE_MEM_SIZE (1<<20)  // reserve a megabyte
static void *gSpareMem;

// ------------------------------------------------------------------------------------------------
void *tenacious_malloc(int requested_allocation_size)   {
    static int remaining_spare_size = 0;    // SPARE_MEM_SIZE;
    char err_msg[512];
    void *rtn = NULL;

    // attempt to re-establish the full size of spare memory, if it needs it
    if (SPARE_MEM_SIZE != remaining_spare_size) {
        if(NULL != (gSpareMem = realloc(gSpareMem, SPARE_MEM_SIZE))) {
            remaining_spare_size = SPARE_MEM_SIZE;
            // "touch" the memory so O/S will allocate physical memory
            meset(gSpareMem, 0, SPARE_MEM_SIZE);
            printf("\nSize of spare memory pool restored successfully in %s:%s at line %i :)\n",
                            __FILE__, __FUNCTION__, __LINE__);
        }   else   {
            printf("\nUnable to restore size of spare memory buffer.\n");
        }
    }
    // attempt a plain, old vanilla malloc() and test for failure
    if(NULL != (rtn = malloc(requested_allocation_size))) {
        return rtn;
    }   else  {
        sprintf(err_msg, "\nInitial call to malloc() failed in %s:%s at line %i",
                                                __FILE__, __FUNCTION__, __LINE__);
        if(remaining_spare_size < requested_allocation_size)    {
            // not enough spare storage to satisfy the request, so no point in trying
            printf("%s\nRequested allocaton larger than remaining pool. :(\n\t --- ABORTING --- \n", err_msg);
            return NULL;
        }   else   {
            // take the needed storage from spare memory
            printf("%s\nRetrying memory allocation....\n", err_msg);
            remaining_spare_size -= requested_allocation_size;
            if(NULL != (gSpareMem = realloc(gSpareMem, remaining_spare_size))) {
                // return malloc(requested_allocation_size);
                if(NULL != (rtn = malloc(requested_allocation_size))) {
                    printf("Allocation from spare pool succeeded in %s:%s at line %i :)\n",
                                            __FILE__, __FUNCTION__, __LINE__);
                    return rtn;
                }   else  {
                    remaining_spare_size += requested_allocation_size;
                    sprintf(err_msg, "\nRetry of malloc() after realloc() of spare memory pool "
                        "failed in %s:%s at line %i  :(\n", __FILE__, __FUNCTION__, __LINE__);
                    return NULL;
                }
            }   else   {
                printf("\nRetry failed.\nUnable to allocate requested memory from spare pool. :(\n");
                return NULL;
            }
        }
    }   
}
// ------------------------------------------------------------------------------------------------
int _tmain(int argc, _TCHAR* argv[])    {
    int     *IntVec = NULL;
    double  *DblVec = NULL;
    char    *pString = NULL;
    char    String[] = "Every good boy does fine!";

    IntVec = (int *) tenacious_malloc(100 * sizeof(int));
    DblVec = (double *) tenacious_malloc(100 * sizeof(double));
    pString = (char *)tenacious_malloc(100 * sizeof(String));

    strcpy(pString, String);
    printf("\n%s", pString);


    printf("\nHit Enter to end program.");
    getchar();
    return 0;
}
user2548100
  • 4,571
  • 1
  • 18
  • 18
  • 5
    Why is this labelled C++ if you are using `malloc`? In C++ you can have different new handlers – Ed Heal Jan 16 '14 at 20:44
  • 5
    Write the whole program without using the heap in the first place. Many safety critical systems adopt this idea. just needs a different mindset – Ed Heal Jan 16 '14 at 20:45
  • 1
    You could use memory pools. – Colin D Bennett Jan 16 '14 at 20:46
  • 1
    @EdHeal, presumably, if that were an option, this wouldn't be a problem? Advice to "Ride the bus" doesn't help me fix my flat? – user2548100 Jan 16 '14 at 20:48
  • 1
    @user2548100 - Sometimes it is worth taking a step back to get a bigger picture. There is another option - just buy some more memory. But if you feel that this is going to be an issue then perhaps a redesign and philosophy is required – Ed Heal Jan 16 '14 at 20:52
  • 2
    @user2548100 We did, what Ed Heal's proposing (redesign components for usage on small MCs with limited RAM) at work, and all I can tell you is: It's worth the efforts and works pretty well. The only memory that's actually 'allocated' in our system, is the stack available for a particular thread (RTOS task). All the rest, is a matter of getting the necessary stack sizes right for the particular application. – πάντα ῥεῖ Jan 16 '14 at 20:58
  • @πάνταῥεῖ - Just have upper bounds, no recursion etc. Works well and in you can work out the upper bound for the stack size that is required. Welcome to writing software for power stations – Ed Heal Jan 16 '14 at 21:05
  • @EdHeal Faced that for Cortex Mx applications currently. All C++(11) on top, works pretty well. I was so proud about proving that ;) ... – πάντα ῥεῖ Jan 16 '14 at 21:08
  • Very nice comments! We actually had one of the "Unicorn" cases here at work, where there is a known memory leak in Oracle, so even though on a massive server with hundreds of gigs of memory I got no actionable intelligence from my error traps. This would have solved that problem, but I'm always happier about making systemic code changes if I know what the experience of others has been. – user2548100 Jan 16 '14 at 21:26
  • 3
    With all the other comments on this covering so many good things, all I have to offer is a +1 for the *outstanding* choice of naming your function `tenacious_malloc()` – WhozCraig Jan 16 '14 at 22:41
  • 1
    @EdHeal: Buying more RAM doesn't help if you're running out of address space. – Eric Lippert Jan 16 '14 at 23:33
  • The technique is reasonable, but it might help to take a step back. The presupposition of the question is that you're in trouble because you don't know the bound on how much memory you're going to need, and there is a realistic scenario in which you take too much. If you eliminate that scenario then you don't need the special handler. If you can't eliminate it then my recommendation is to eliminate it on the error recovery path. Know *exactly* how much memory you are going to need on the error recovery path ahead of time and pre-allocate *exactly* that much. – Eric Lippert Jan 16 '14 at 23:39
  • 1
    @EricLippert, good points, but during our recent debacle, it wasn't my code or process that was creating memory exhaustion, it was Oracle's memory leak. In that case, tenacious_malloc() bets it can realloc() the spare memory and allocate it for your use before the leaking code can hog up the spare memory as well. It's a bit of a game of chance, which is why I was asking if anyone had experience with a technique like this. What I find very satisfying about this, is I now have ACTIONABLE info when malloc() fails. Otherwise, what's the point of trapping those errors? – user2548100 Jan 16 '14 at 23:58
  • 1
    @EricLippert, like most shops we use things like binary trees, linked lists, etc, which precludes knowing memory requirements in advance. One of the really wonderful things about trees is they are self-maintaining - so long as you don't run out of memory, or suffer fatal fragmentation. I may have erred in targeting this approach towards embedded code, as I now recall dynamic memory isn't commonly used there. My bad. :( – user2548100 Jan 17 '14 at 00:03
  • Am I the only one who finds the reference to Kesselring out of place? http://en.wikipedia.org/wiki/Piero_Calamandrei – Nicola Musatti Jan 17 '14 at 00:16
  • @NicolaMusatti, I hope that wasn't offense in any way. His was a remarkably successful orderly retreat that prevented an Allied link-up of forces in Italy with those in France, even after D-Day, and right to the end of the war. It got me thinking there must be a better way to handle memory exhaustion than immediately crashing the process. – user2548100 Jan 17 '14 at 00:55
  • @EdHeal, I used to use new() and delete() until I was stepping thru the STL code for an ordered map and noticed the STL lib was calling malloc() and free(). Obviously it's best to use something that fits with the context your code is being written in, but under the covers these two things are synonyms AFAICT. –  Jan 18 '14 at 21:48
  • @RocketRoy - but new/delete calls the constructors/destructors. But then again you can write the program without either – Ed Heal Jan 18 '14 at 21:54
  • @EdHeal, do you have an implementation of a binary tree that doesn't use the heap? Linked-list? – user2548100 Jan 20 '14 at 22:18
  • Reading "NUMA aware heap memory manager" by Patryk Kaminski, the tenacious_malloc() approach looks promising. From pg 6... – user2548100 Jan 21 '14 at 00:13
  • "When a small memory allocation request comes in (32 KB or less,) the heap manager first checks to see if it can be completed from the local cache. If not, it checks the central cache (at this point it uses lock synchornization). If the central cache does not have a free object of the specified size, it checks the page heap. If the page heap does not have a a free memory object of sufficiently large size, TCMalloc uses an OS specific API to allocate another large block of memory (typically 1 MB or more). – user2548100 Jan 21 '14 at 00:13
  • The memory is then divided into smaller chunks and moved to one of the free lists in the central cache. Next, a portion of free memory objects of the predefined size is moved from the free list to the local cache for the calling thread. For large memory allocations, TCMalloc bypasses the local and central cache and immediately tries to use the page heap to try to complete the request.... end quote. – user2548100 Jan 21 '14 at 00:14
  • I've given you a+1 for *checking* the return value of `malloc()` - too often people don't do that, assuming they'll get what they ask for, then not detect the NULL-pointer :-( – Andrew Jun 27 '23 at 06:11
  • See also https://stackoverflow.com/questions/60742161/how-to-handle-malloc-failing-and-returning-null – Andrew Jun 27 '23 at 06:12

3 Answers3

1

The best strategy is to aim for code that works without allocations. In particular, for a correct, robust program, all failure paths must be failure-case-free, which means you can't use allocation in failure paths.

My preference, whenever possible, is to avoid any allocations once an operation has started, instead determining the storage needed and allocating it all prior to the start of the operation. This can greatly simplify program logic and makes testing much easier (since there's a single point of possible failure you have to test). Of course it can also be more expensive in other ways; for example, you might have to make two passes over input data to determine how much storage you will need and then process it using the storage.

In regards to your solution of pre-allocating some emergency storage to use once malloc fails, there are basically two versions of this:

  1. Simply calling free on the emergency storage then hoping malloc works again afterwards.
  2. Going through your own wrapper layer for everything where the wrapper layer can directly use the emergency storage without ever freeing it.

The first approach has the advantage that even standard library and third-party library code can utilize the emergency space, but it has the disadvantage that the freed storage could be stolen by other processes, or threads in your own process, racing for it. If you're sure the memory exhaustion will come from exhausting virtual address space (or process resource limits) rather than system resources, and your process is single-threaded, you don't have to worry about the race, and you can fairly safely assume this approach will work. However, in general, the second approach is much safer, because you have an absolute guarantee that you can obtain the desired amount of emergency storage.

I don't really like either of these approaches, but they may be the best you can do.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • is there a way to implement #2 using the standard C memory functions like malloc(), calloc(), realloc()? (or C++ new and delete for that matter). Maybe I missed something? – user2548100 Jan 17 '14 at 17:19
  • PS: R, I looked up your linked website off your profile. Impressive work. – user2548100 Jan 17 '14 at 18:24
  • There's no portable way to hook into the standard allocator and add functionality like #2. There may be system-specific ways on some systems, but even then, you'd have to have a way to know whether the caller you're handing out the emergency space to is really a caller you want to give it to, or just something unimportant (possibly even the culprit that's exhausting memory). For both of these reasons I think alternate approaches are much better. – R.. GitHub STOP HELPING ICE Jan 17 '14 at 20:12
  • The use of realloc() instead of free() seems more in the spirit of an orderly retreat. It "should" allow multiple calls to malloc() to succeed, provided there is enough spare memory. I'm tempted to use tenacious_malloc() and just turn off the virtual system on a few apps. –  Jan 18 '14 at 21:38
  • You mean using `realloc` on the emergency block to convert it into a small block? That's probably even less reliable; if the emergency block is large (an implementation-specific threshold), it's possible that it can't be used to satisfy small allocations, so `realloc` might internally have to do `malloc`+`free`, which would fail. – R.. GitHub STOP HELPING ICE Jan 18 '14 at 22:13
  • That makes no sense at all. Having a large block virtually guarantees it can satisfy any request for a small block. It's not going to malloc to find more space, since it already has plenty relative to the requested size. Once you free the reserve block, all bets are off, as the big, contiguous chunk allocated off the heap at the beginning of the program is gone, and any new malloc will have to compete with any other request for memory. I like the use of realloc() in the OPs code much better, –  Jan 19 '14 at 01:08
  • @RocketRoy: Formally, `realloc` is identical to `malloc`+`memcpy`+`free`, and in some implementations, this is how it will *always* be performed. For example on OpenBSD's omalloc implementation, this will almost always be the case (except when the old and new size, after rounding, are the same) since is segregates allocations by size. – R.. GitHub STOP HELPING ICE Jan 19 '14 at 05:51
  • On other less-spartan implementations, you still have similar issues. Most dlmalloc-like allocators use dedicated `mmap` mappings for large allocations but use a carved-up heap for smaller ones. Converting a dedicated map into heap space that can be carved up into small chunks is non-trivial, and attempting to do so may actually lead to pathological fragmentation, so it's a rather bad thing for an implementation to do. – R.. GitHub STOP HELPING ICE Jan 19 '14 at 05:53
  • @R, I don't think this is an argument against the tenacious_malloc() approach. It argues instead that a large block and small block path may be needed, one which makes best use of the environment's heap manager. If, for example, a heap manager gets space for small allocations from it's big-brother big-chunk manager, then a list of small chunks can be used for emergencies, and the request size determines which path through the tenacious_malloc() code is used. –  Jul 25 '14 at 21:51
1

On a modern 64 bit computer, you can malloc significantly more memory than you have RAM. In practice, malloc doesn't fail. What happens in practice is that your application starts thrashing, and once you have say 4GB of RAM and your allocations exceed that, your performance will drop to zero because you are swapping like mad. Your performance goes down so much that you never get to the point where malloc can't return memory.

gnasher729
  • 51,477
  • 5
  • 75
  • 98
  • Any idea what happens with an SSD drive? IIRC, Windows has an API call to determine how much physical memory remains, which could be used to stop thrashing. Windows also support multiple heaps, a great feature which no one seems to know about. This would allow tenacious_malloc() to work from its own heap. The malloc()s in tenacious_malloc() should probably be calloc()s though, as Drepper has pointed out, calloc() forces memory to be committed, not just mapped. –  Jul 25 '14 at 21:55
0

"On a modern 64 bit computer, you can allocate, e.g. by calling malloc(), significantly more memory than your physically installed RAM."

Note that RAM does not equal virtual memory, which brings us to the next point.

Whether one can obtain more virtual memory than physically available RAM is operating system dependent: GNU/Linux operating systems usually come misconfigured out of the box to over provision memory, which is an incorrect behavior in any scenario, and is particularly detrimental in mission critical deployments.

All traditional UNIX systems will not allow for requesting more virtual memory than is available, and will return ENOMEM, which is the correct behavior, because it means that no hacks like an "out of memory killer" are necessary, which in turn means that running processes will continue to function, provided they already have all the memory they require.

pmacfarlane
  • 3,057
  • 1
  • 7
  • 24
Annatar
  • 1
  • 1