Click here to Skip to main content
15,893,486 members
Articles / Programming Languages / C#
Tip/Trick

Improving on .NET Memory Management for Large Objects

Rate me:
Please Sign up or sign in to vote.
5.00/5 (21 votes)
8 Apr 2015CPOL5 min read 57.6K   581   49   16
How to improve on .NET memory management for large objects

Introduction

As documented elsewhere, .NET memory management consists of two heaps: the Small Object Heap (SOH) for most situations, and the Large Object Heap (LOH) for objects around 80 KB or larger.

The SOH is garbage collected and compacted in clever ways I won't go into here.

The LOH, on the other hand, is garbage collected, but not compacted. If you're working with large objects, then listen up! Since the LOH is not compacted, you'll find that either:

  1. you're running an x86 program and you get OutOfMemory exceptions, or
  2. you're running x64, and memory usage becomes unacceptably high. Welcome to the world of a fragmented heap, just like the good old C++ days.

So where does this leave us? Let's code our way out of this jam!

If you work with streams much, you know that MemoryStream uses an internal byte array and wraps it in Stream clothing. Memory shenanigans made easy! However, if a MemoryStream's buffer gets big, that buffer ends up on the LOH, and you're in trouble. So let's improve on things.

Our first attempt was to create a Stream-like class that used a MemoryStream up to 64 KB, then switched to a FileStream with a temp file after that. Sadly, disk I/O killed the throughput of our application. And just letting MemoryStreams grow and grow caused unacceptably high memory usage.

So let's make a new Stream-derived class, and instead of one internal byte array, let's go with a list of byte arrays, none large enough to end up on the LOH. Simple enough. And let's keep a global ConcurrentQueue of these little byte arrays for our own buffer recycling scheme.

MemoryStreams are really useful for their dual role of stream and buffer so you can do things like...

C#
string str = Encoding.UTF8.GetString(memStream.GetBuffer(), 0, (int)memStream.Length);

So let's also work with MemoryStreams, and let's keep another ConcurrentQueue of these streams for our own recycling scheme. When a stream to recycle is too big, let's chop it down before enqueuing it. As long as the streams stay under 8X of little buffer size, we just let it ride, and the folks requesting streams get something with Capacity between the little buffer size and 8X the little buffer size. If a stream ends up on the LOH, we pull it back to the SOH when it gets recycled.

Finally, for x86 apps, you should have a timer run garbage collection with LOH defragmentation like so:

C#
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

We run this once a minute for one of our x86 applications. It only takes a few milliseconds to run, and, in conjunction with buffer and stream recycling, memory usage stays manageable.

Check out the attached class library for the Stream-derived class, BcMemoryStream, and the overall memory manager, BcMemoryMgr. There's a third class, an IDisposable class call MemoryStreamUse, which manages recycling of MemoryStreams.

You just tell BcMemoryMgr how big you want the little buffers to be and how many buffers and MemoryStreams to recycle. A buffer size of 8 KB is good because 8 X 8 KB -> 64 KB max size for MemoryStreams, which is under the 80 KB LOH threshold. You have to make peace with the max number of objects to keep in the recycling queues. For an x86 program and 8 KB buffers and heavy MemoryStream use, you might not want to allow a worst case of 8 X 8 KB X 10,000 -> 640 MB to get locked up in this system. With a maximum queue length of 1,000, you're only committing to a max of 64 MB, which seems a low price to pay for buffer and stream recycling. To be clear, the memory is not pre-allocated; the max count is just how large the recycling ConcurrentQueues can get before buffers and streams are let go for normal garbage collection.

Let's look at each class in detail.

Let's start with BcMemoryMgr. It's a small static class. It has ConcurrentQueues for recycling buffers and streams, the buffer size, and the max queue length.

Looking at the member functions, you Init with the buffer size and max queue length, and it calls a SelfTest routine that tests the class library. If the tests don't pass, the code doesn't run...poor man's unit testing. Note that you can specify a zero max queue length, in which case you'll get no recycling, just normal garbage collection. There are buffer functions AllocBuffer and FreeBuffer ... anybody remember malloc and free? There are stream functions AllocStream and FreeStream. FreeStream chops the stream down if it's too big before enqueuing it for reuse.

BcMemoryStream is Stream-derived and implements pretty much the same interface as MemoryStream. One notable exception is that you cannot set the Capacity property. Instead, there is a Reset function you can call to free all buffers in the class, returning the Capacity to zero. The fun code is in Read and Write; Buffer.BulkCopy came in handy. There are extension routines for working with strings. These were handy when writing BcMemoryMgr's SelfTest routine.

BcMemoryStreamUse is a small IDisposable class uses BcMemoryMgr to allocate a MemoryStream in its constructor and free it in its Dispose method. Stream recycling is fun and easy!

BcMemoryBufferUser is similar to BcMemoryStream, just for byte arrays. Buffer recycling is fun and easy!

BcMemoryTest puts BcMemoryStream and MemoryStream up to a test, hashing all files in a directory and its subdirectories. For each file, it starts with a FileStream, then CopyTo's into either MemoryStream or BcMemoryTest, the does the hashing. In our tests, we use a leafy and varied test directory with lots of built binaries. The performance for the total run time for BcMemoryStream was about 25% faster than MemoryStream. So not only did we solve the LOH problem, we get a better end result. Hooray!

Finally, there's a little config file addition that is absolutely necessary for server applications:

XML
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    ...
    <runtime>
        <gcServer enabled="true"/>
    </runtime>
</configuration>

This gives you background garbage collection, a heap per processor, etc. A real lifesaver.

Hope this helps!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
Michael Balloni is a manager of software development at a cybersecurity software and services provider.

Check out https://www.michaelballoni.com for all the programming fun he's done over the years.

He has been developing software since 1994, back when Mosaic was the web browser of choice. IE 4.0 changed the world, and Michael rode that wave for five years at a .com that was a cloud storage system before the term "cloud" meant anything. He moved on to a medical imaging gig for seven years, working up and down the architecture of a million-lines-code C++ system.

Michael has been at his current cybersecurity gig since then, making his way into management. He still loves to code, so he sneaks in as much as he can at work and at home.

Comments and Discussions

 
QuestionRecyyclable memory stream Pin
matra215-Apr-15 5:24
matra215-Apr-15 5:24 
AnswerRe: Recyyclable memory stream Pin
Michael Sydney Balloni16-Apr-15 8:41
professionalMichael Sydney Balloni16-Apr-15 8:41 
Questionvirtual memory Pin
Dávid Kocsis14-Apr-15 23:33
Dávid Kocsis14-Apr-15 23:33 
AnswerRe: virtual memory Pin
Michael Sydney Balloni16-Apr-15 8:44
professionalMichael Sydney Balloni16-Apr-15 8:44 
QuestionShould this be called "Improved MemoryStreams"? Pin
Paulo Zemek14-Apr-15 8:01
mvaPaulo Zemek14-Apr-15 8:01 
AnswerRe: Should this be called "Improved MemoryStreams"? Pin
Michael Sydney Balloni16-Apr-15 8:50
professionalMichael Sydney Balloni16-Apr-15 8:50 
GeneralRe: Should this be called "Improved MemoryStreams"? Pin
Paulo Zemek16-Apr-15 9:17
mvaPaulo Zemek16-Apr-15 9:17 
As I tried to explain, what happens if you recycle buffers but still allow the LOH to be used?
I am pretty sure you will also solve your problem, as when you recycle the buffers, you avoid new allocations, avoiding new memory fragmentation.

About the GC.Collect, probably only doing the GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; without forcing the collection will do a better job. You will simply instruct that a future full collection will need to compress the Large Object Heap, without forcing an immediate collection. I can tell you that I used servers that took hours to do a full collection, simply because I avoided allocating new objects for no reason.
Forcing collections every minute is probably too much (and if the .NET is about to get an Out of Memory exception, it will force a full collection anyway).

About the collection only taking a few milliseconds, well, the less objects you have and the less fragmented the memory is, the faster the collection will be. It is hard to test it. But if that was a real solution to all cases, I am sure .NET will do that by default. Again, it is possible that it works in certain scenarios.

And about large objects of varying size... well, that's another possible optimization: Try to avoid objects with different sizes. If your objects use 1mb each, but exactly 1mb, then it becomes easy to reuse free spots.

I can say that for a session manager application, I had the following:
Every second (yes, every second), I verified if the total memory was above a certain threshold. If it was, I forced a full garbage collection.
Every session data was hold with weak-references and forced to stay alive the next garbage collection when they were used, but they will be allowed to be collected after two collections without use.
As I avoided creating new objects when one was already in memory, the need for garbage collections was minimal.
If the application did use more memory than it was allowed to, then it would keep doing garbage collections until the memory was free.
Considering that most objects were actually collectible, I never stuck into a loop forcing garbage collections when it was impossible to collect objects. In fact, after 2 garbage collections things went from 6 gb (yes, that was the limit I established) to 100mb. Considering I had 16gb to use, I still had room to allow the application to use more memory.
In practice, with real sessions, I never had to use the forced garbage collections, as the normal garbage collection did the job. Yet I did tests forcing really huge sessions to see that everything worked.
I understand that 32-bit servers would have a different reality, but simply forcing collections every minute is far from ideal to most servers.
GeneralRe: Should this be called "Improved MemoryStreams"? Pin
Michael Sydney Balloni16-Apr-15 9:25
professionalMichael Sydney Balloni16-Apr-15 9:25 
GeneralRe: Should this be called "Improved MemoryStreams"? Pin
Paulo Zemek16-Apr-15 9:37
mvaPaulo Zemek16-Apr-15 9:37 
GeneralRe: Should this be called "Improved MemoryStreams"? Pin
Michael Sydney Balloni16-Apr-15 10:22
professionalMichael Sydney Balloni16-Apr-15 10:22 
QuestiondotTraced? Pin
Leonid Ganeline10-Apr-15 6:51
professionalLeonid Ganeline10-Apr-15 6:51 
AnswerRe: dotTraced? Pin
Michael Sydney Balloni16-Apr-15 8:54
professionalMichael Sydney Balloni16-Apr-15 8:54 
QuestionNice solution to an age old problem Pin
Keith Vinson9-Apr-15 8:38
Keith Vinson9-Apr-15 8:38 
AnswerRe: Nice solution to an age old problem Pin
Michael Sydney Balloni16-Apr-15 8:57
professionalMichael Sydney Balloni16-Apr-15 8:57 
GeneralMy vote of 5 Pin
GerVenson9-Apr-15 2:37
professionalGerVenson9-Apr-15 2:37 
GeneralRe: My vote of 5 Pin
Michael Sydney Balloni16-Apr-15 9:00
professionalMichael Sydney Balloni16-Apr-15 9:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.