Debugging: Diagnosing why malloc is failing


A customer had some code which was experiencing memory allocation failures when calling malloc (which maps to Heap­Alloc). The function returns nullptr, and Get­Last­Error() reports ERROR_NOT_ENOUGH_MEMORY. However, there was still plenty of memory free:

  • Task Manager reported working set at around 400MB, with a peak of 550MB.
  • Using the _heap­walk function to compute the total memory used resulted in about 380MB being reported.
  • The _heap­chk function reported no errors.
  • The virtual memory size for the process was a little bit more than the working set size.

The customer was continuing their investigation but was looking for some pointers since the bug took a day to emerge. Could it be heap fragmentation? (The program is uses the regular C runtime heap and does not enable the low-fragmentation heap.)

One of the suggestions was to run the VMMap utility to see if the problem was exhaustion of virtual address space.

And lo and behold, that was indeed the cause. The code had a bug where it was leaking threads. Since the default stack reservation for a thread is 1MB (although typically only a tiny fraction of that ends up being committed and even less being charged against working set), a slow accumulation of threads corresponds to a slow erosion of the virtual address space until you eventually run out.

Once again, it’s the address space, stupid.

Comments (14)
  1. Henke37 says:

    And here I was going to blame the code for asking for five bajillion gigabytes of memory.

  2. James Chaldecott says:

    Yeah, we had that problem, once.

    We were using the .Net Parallel.For() method to process a large (> 4M) set of items, where the processing for each item was reasonably time consuming, such that the entire job took at least an hour. The processing was CPU bound.  

    Under those conditions the .Net thread pool kept adding more threads, until we had several hundred and ran out of address space and started getting OOM exceptions (actually, we usually got failures when trying to map views onto memory mapped files).

    We used VMMap to diagnose the OOM problem, too. I was quite surprised to see > 400MB under the "Stack" heading!

    The simple fix was to restrict the maximum level of parallelism in the Parallel.For() call.

  3. Gabe says:

    I would assume you'd get back that 1MB reservation once the thread terminates and all handles to it are closed. Is there any time that assumption would be wrong?

  4. Nick says:

    @James: I think they fixed that basic problem with the thread pooling updates in Windows 8.1, but limiting the parallelism was probably a good idea too (perhaps to System.Environment.ProcessorCount or some derivative thereof).

  5. Joshua says:

    @James: Bad thread pool. Under no circumstances when running a CPU bound job should there be more than 2x the number of threads than schedulable cores.

  6. jader3rd says:

    @Joshua,

    That's not a bad thread pool, that's bad use of the thread pool. The thread pool doesn't know if you'll be running a CPU bound job, or if you're requesting for a thread that'll sit around most of the time, just consuming memory.

  7. AsmGuru62 says:

    So, next question would be: "When a thread gets leaked?"

    1. When thread does not return

    2. Thread returns, but its handle never closed.

    etc.

  8. RangerFish says:

    One of the things I do at work is maintain our internal VS Addins. One developer ran over and reported an out of memory error in an addin that looks at files in your TFS pending changes list. I wondered if they had an unusually large list of files checked out, but that wasn't the case. In fact, they had plenty of free memory, and the devenv.exe process didn't seem to be using that much memory (determined using Process Explorer).

    I asked them to use VMMap and sure enough, the process' memory was totally fragmented. I asked them to restart Visual Studio, and that fixed the issue. Once again, the magical reboot comes to the rescue!

  9. Karellen says:

    OK, I'm missing something here. Can someone point me to a resource which explains how the leaked thread stack space can take up virtual address space without counting towards the virutal memory size of the process?

  10. Michael says:

    Karellen: Task Manager doesn't have a stat that reports the address space consumed by a process, just the working set, private working set, and commit size (which I think used to be called VM size in XP, and probably also what Raymond is referring to in the fourth bullet), all of which will be tiny for most threads, as Raymond notes. PerfMon / Process Explorer / VMMap / other tools are needed to see the address space consumed.

    Gabe/AsmGuru: The thread just needs to actually exit for the address space to be freed (at least, the stack, other resources associated with the thread will linger). At least, that's the observable behavior.

  11. KC says:

    A thread's stack has to be a contiguous chunk of virtual address space, so the thread reserves a section of virtual address space.  It doesn't commit memory (either physical or space in the page file) until it needs it.

    According to another poster, the reservation is for 1 MB even though most threads use much less than that, and so committed memory is much smaller.

     KC

  12. Little Joke says:

    "but was looking for some pointers"

    I'm surprised nobody commented on this (potential) little joke.

  13. Cheong says:

    @Little Joke: Why? I think it's polite way of asking question with expectation of reply just pointing to some reference materials, and no spoonfeeding is needed.

  14. Drak says:

    @cheong00: Memory, Pointers, C(++), get it? Asking for 'pointers' on a memory problem is potentially funny.

Comments are closed.