{"id":1703,"date":"2013-10-02T00:00:00","date_gmt":"2013-10-02T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/large-object-heap-compaction-should-you-use-it\/"},"modified":"2021-05-17T18:36:05","modified_gmt":"2021-05-17T18:36:05","slug":"large-object-heap-compaction-should-you-use-it","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/development\/dotnet-development\/large-object-heap-compaction-should-you-use-it\/","title":{"rendered":"Large Object Heap Compaction: Should You Use it?"},"content":{"rendered":"<div class=\"article-content\">\n<p class=\"start\">   When we talk  about heap memory in .NET it&#8217;s natural to picture the heap as a single large  contiguous block of memory. However, given that it has been carefully  architected in order to optimise performance, this isn&#8217;t quite true. Instead,  .NET breaks down the heap into 4 separate chunks, the first three of which are  known as the small object heaps (SOHs), and are referred to as generation 0, 1,  and 2 respectively. We&#8217;ll be focusing on the fourth heap, which is known as the  large object heap (LOH) and is used to store all objects that are larger than  85,000 bytes.<\/p>\n<h2>.NET Memory in a Nutshell<\/h2>\n<p>If you&#8217;re  already familiar with generational garbage collection, you can skip ahead to  the next section, but if you&#8217;d like a primer \/ refresher than stay with me for  a moment. The reason for segmenting memory in this manner is to reduce the  performance cost of garbage collection. Empirical studies have shown that, for  any realistic application, it tends to be the case that the objects that have  been most recently created are the most likely to be destroyed, meaning that it&#8217;s  advantageous to garbage collect recently allocated objects more often that the  ones that have already been around for a while.<\/p>\n<p>   By dividing  the SOH into the 3 separate generations, it is possible for the garbage  collector to collect only certain parts of the SOH (thus lowering the performance  cost) rather than scanning everything each time a collection happens. In short,  when a new object is instantiated onto the SOHs it is placed on generation 0.  If it then survives a garbage collection it is &#8216;promoted&#8217; to generation 1, and  if it survives a second garbage collection it will be promoted to generation 2.<\/p>\n<div class=\"note\">\n<p class=\"note\">   This is a slight simplification, some objects may remain in  their current generation as they could be pinned, or added to the finalizer  queue, or created during the garbage collection itself.<\/p>\n<\/p><\/div>\n<p>   A generation  0 collection will happen when generation 0 is full, and a generation 1  collection will happen when generation 1 is full and will also collect  generation 0. Similarly generation 2 collections also collect all lower  generations, and thus is relatively expensive to do. Thankfully, the CLR tracks  your application&#8217;s memory allocations at run time and continually tunes the  size of the various generations for maximum performance, and also decides when  to perform generation 2 collections. After a collection, any remaining objects  on the SOHs are &#8216;compacted&#8217;, meaning they are shuffled up against each other to  remove any gaps in memory. This means that the CLR can allocate only as much  memory as is actually needed, rather than try and fit into new and promoted  objects into awkwardly sized gaps (known as fragmentation).<\/p>\n<p>   The LOH is  also collected when a generation 2 collection happens, but unlike the small  object heaps the large object heap isn&#8217;t compacted when it is garbage  collected, which means that the LOH can get into a fragmented state. This is a  problem because if the heap is sufficiently fragmented there will be no gaps  large enough for new objects to be allocated into so new objects will have to  be allocated at the end of the heap, thereby causing the heap to expand. If  this process repeats continually the LOH will eventually consume all the  system&#8217;s available memory and the program will crash with an OutOfMemory  exception.<\/p>\n<p>   For a more  thorough understanding of .NET memory, check out this piece on the <a href=\"http:\/\/www.red-gate.com\/products\/dotnet-development\/ants-memory-profiler\/learning-memory-management\/memory-management-fundamentals?utm_source=simpletalk&amp;utm_medium=publink&amp;utm_campaign=antsmemoryprofiler&amp;utm_content=LOHcompaction-ChrisMorter\">Top 5 .NET Memory management  Fundamentals<\/a>.<\/p>\n<h3>Why is LOH fragmentation so bad?<\/h3>\n<p>LOH  fragmentation can be a difficult problem to tackle since. NET abstracts away  the concept of physical memory locations. This makes it hard for the developer  to figure out where the CLR may choose to allocate objects, and even harder to  find out which particular allocation patterns are resulting in gaps being left  in the LOH. To make troubleshooting harder, any potential problems tend to  require the program to have been running for a length of time before becoming  apparent, which makes debugging a tedious process.<\/p>\n<p>   For more  details on the dangers of LOH fragmentation, I recommend reading this excellent  post by Andrew Hunter: &#8220;<a href=\"https:\/\/www.simple-talk.com\/dotnet\/.net-framework\/the-dangers-of-the-large-object-heap\/?utm_source=simpletalk&amp;utm_medium=publink&amp;utm_campaign=antsmemoryprofiler&amp;utm_content=LOHcompaction-ChrisMorter\">The dangers of the Large Object Heap<\/a>&#8220;. <\/p>\n<h2>So what can you do to avoid LOH fragmentation?<\/h2>\n<p>Generally,  solving LOH fragmentation problems requires following one of the three strategies:<\/p>\n<ul>\n<li>Figure  out which large objects are responsible for fragmentation, and then break them  down into smaller parts which are assimilated into a functionally equivalent  wrapper class.<\/li>\n<li>Re-architect  parts of the application to reduce the churn of large objects<\/li>\n<li>Periodically  restart the application (this is essentially what recycling app pools for  ASP.NET applications seeks to achieve)<\/li>\n<\/ul>\n<p>Each of  these solutions are either difficult, inelegant, laborious, or a combination  thereof. However, in .NET 4.5.1 the .NET team at Microsoft has provided another  possibility by adding the ability to easily do a one-off garbage collection, <strong>followed by a LOH compaction<\/strong>, with the  following code:<\/p>\n<p>   <code>GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;   GC.Collect(); \/\/ This can be omitted <\/code> <\/p>\n<p>If <strong>GC.Collect()<\/strong> is omitted then the LOH  compaction will happen when the next LOH garbage collection occurs naturally. After  this modified garbage collection has finished the application will continue  running as before (i.e. with no LOH compactions).<\/p>\n<p>   Microsoft  deliberately chose <em>not<\/em> to compact the  LOH by default when it is garbage collected (unlike the small object heaps) because  they believe that the performance impact of regularly performing a LOH  compaction outweighs the benefits of doing so. In an <a href=\"http:\/\/blogs.msdn.com\/b\/dotnet\/archive\/2013\/06\/26\/announcing-the-net-framework-4-5-1-preview.aspx\">MSDN blog announcing the release of  .NET 4.5.1<\/a>, the  Microsoft .NET team give the following warning concerning using LOH compaction:<\/p>\n<p>       &#8220;<em>LOH compaction can be  an expensive operation and should only be used after significant performance  analysis, both to determine that LOH fragmentation is a problem, but also to  decide when to request compaction.<\/em>&#8221;   <\/p>\n<p>Here I will  seek to explain in more detail what actually happens during a LOH compaction,  and clarify when it is appropriate to use it.<\/p>\n<h3>So how long does a compaction take?<\/h3>\n<p>To  investigate the performance hit of LOH compaction I wrote a simple test  application targeted to .NET 4.5.1 which instantiates a random number (100&#194;&#177;40)  of &#160;randomly sized large objects (84KB  &lt;= size &lt; 16MB) , and then subsequently removes a random selection of  them, thereby leaving the LOH in a fragmented state.<\/p>\n<p>  We can infer  the duration of a LOH compaction by comparing the time taken for a standard GC with  the time taken for a GC with LOH compaction since the difference will  presumably be the length of time taken by the compaction. For this approach to  be useful the heaps must be in a consistent state before each trial, so I made  sure to instantiate the same random selection of objects, and then performed a  full garbage collection before each trial.<\/p>\n<p> Repeating  this process for 20 such random starting states showed the following  correlation: <\/p>\n<p class=\"illustration\"> <img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1877-LOHim1.PNG\" alt=\"1877-LOHim1.PNG\" \/><\/p>\n<p class=\"caption\">Figure 1: Results from of LOH  compaction tests.<\/p>\n<p>As you may  expect, there is a strong linear correlation between how long a LOH compaction  takes and the amount of data that has to be moved. To quantify the amount of  data moved we must consider what happens during a compaction.<\/p>\n<h3>The compaction algorithm<\/h3>\n<p>During a  compaction the [compaction] algorithm will look through the LOH until it finds  a gap, at which point it will take the next object along the heap and simply move  it down to fill the gap. It will then continue looking through the heap,  continually shifting objects down each time it encounters a gap. Note that this  has the effect that, if there is a gap near the start of the LOH (as will  probably be the case assuming that the gaps are numerous and uniformly  distributed), then the <em>majority of the  data in the LOH will end up being moved<\/em>.<\/p>\n<p>   The  practical consequence of this is that compacting a slightly fragmented heap will  require moving nearly as much data as compacting a very badly fragmented heap,  and so will take roughly the same length of time. This means that performing  frequent compactions doesn&#8217;t make subsequent compactions quicker, so you should  delay performing compactions until it is really necessary (if at all).<\/p>\n<p>   The  compactions took around 2.3ms per MB moved on the LOH on my desktop (i5-3550  CPU with 16GB of DDR3 memory). Using a tool like ANTS Memory Profiler it is  possible to measure the size of objects on the LOH, and so estimate how long your  application may freeze for due to a LOH compaction: <\/p>\n<p>One  interesting feature of compactions, both on the LOH and SOHs, is that the  objects on the heaps are not reordered during the compaction, even if doing so  would increase the speed of the compactions. The reason for this is to preserve  locality of reference, as objects are likely to be created in a similar order  to that in which they are accessed. In addition, the time required to compute a  suitable reordering would likely offset the potential time saved anyway.<\/p>\n<h3>So when should you use compaction?<\/h3>\n<p>I would  recommend using the LOH compaction only if the following criteria are  satisfied:<\/p>\n<ul>\n<li>You  are already targeting .NET 4.5.1 or can upgrade to it.<\/li>\n<li>Pauses  of the length estimated in the previous section don&#8217;t seriously affect the  usability of your application.<\/li>\n<li>It  is not possible to pursue strategies of breaking large objects down into  smaller chunks, or reducing large object churn.<\/li>\n<\/ul>\n<p>I think this  is a very useful few feature in the .NET framework, and it suggests that more  developers are realising that they need to at least be aware of what&#8217;s  happening beneath all the abstractions if they want to build really great software.  However, I also think that it should be a strategy of last resort.<\/p>\n<h2>Identifying LOH fragmentation with ANTS Memory Profiler 8<\/h2>\n<p>Of course  this is all academic unless you know when LOH fragmentation is actually  occurring, so I&#8217;ll finish by showing how you can use Red Gate&#8217;s <a href=\"http:\/\/www.red-gate.com\/products\/dotnet-development\/ants-memory-profiler\/?utm_source=simpletalk&amp;utm_medium=publink&amp;utm_campaign=antsmemoryprofiler&amp;utm_content=LOHcompaction-ChrisMorter\">ANTS Memory Profiler 8<\/a> &#160;to identify a LOH fragmentation problem. Here  I have profiled the application I used to test the speed of LOH compactions  earlier in this article, and have taken a snapshot after the objects have been  allocated and a selection of them deallocated, leaving the LOH in a fragmented  state. In fact, this screenshot of the ANTS Memory Profiler&#8217;s summary screen  shows all the hallmarks of a badly fragmented LOH:<\/p>\n<p class=\"illustration\"> <a href=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1877-img2forloh.png\"> <img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1877-img2forloh-600px.png\" alt=\"1877-img2forloh-600px.png\" \/><\/a> <\/p>\n<p class=\"caption\">Figure 2: ANTS Memory Profiler  summary screen, with all the indicators of LOH fragmentation.<\/p>\n<p>   I have circled  the salient details in the snapshot above, and if you&#8217;re identifying LOH  fragmentation in your own application then there are a few details you can look  for:<\/p>\n<ol>\n<li>In the &#8216;<strong>Memory fragmentation<\/strong>&#8216; section, ANTS Memory Profiler warns that &#8220;<em>Memory fragmentation is restricting the size  of objects that can allocated<\/em>&#8220;. <\/li>\n<li>In the &#8216;<strong>Largest classes<\/strong>&#8216; section, 146.2MB of memory is listed as free  space, and similarly in the &#8216;<strong>.NET and  unmanaged memory<\/strong>&#8216; section, 146.2MB is listed as unused memory allocated to  .NET. In this situation, free space could be either gaps in the LOH or unused  space at the end of the heap which hasn&#8217;t been returned by the CLR to the OS.  There are innocent explanations as to why there may be free\/unused space, such  as the CLR deciding not to return memory to the OS if it anticipates using it  again soon, especially on systems with lots of spare memory. However, this will  tend to be a relatively small amount and will be transient, so if your system  has a large amount of free memory for a long period of time, it&#8217;s a sign there  could be a LOH fragmentation problem. <\/li>\n<li>The &#8216;<strong>Memory fragmentation<\/strong>&#8216; section shows that 99.9% of free memory is  taken by large fragments, i.e. gaps in the LOH. The fact that this number is  close to 100% suggests that the majority of the free memory hasn&#8217;t been  deliberately kept by the CLR for future allocations and so is genuinely the  result of fragmentation, which confirms our suspicion that fragmentation is  problem for this application.<\/li>\n<\/ol>\n<p>Of course,  most memory fragmentation problems won&#8217;t be quite as obvious as this, but  hopefully it should give you an idea what to look for should you suspect LOH  fragmentation in your own application.<\/p>\n<h2>TL;DR<\/h2>\n<ul>\n<li>Avoid  using compaction if you can. Favour other methods of dealing with LOH  fragmentation such as breaking large objects into smaller ones or reducing  object churn.<\/li>\n<li>If  you have to use compaction then wait until as late as is safely possible before  compacting<\/li>\n<li>The  duration of a compaction (in milliseconds) can be roughly estimated by  multiplying the size of objects on the LOH (in MB) by 2.3<\/li>\n<\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Despite the many benefits of automatic memory management in .NET, there are still a few perils which we must avoid. One of the most common, and frustrating to deal with, is fragmentation of the large object heap. In this article Chris Morter explains what LOH fragmentation is, why it&#039;s a problem, and what you can do to avoid it.&hellip;<\/p>\n","protected":false},"author":37781,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143538],"tags":[4143,4229],"coauthors":[],"class_list":["post-1703","post","type-post","status-publish","format-standard","hentry","category-dotnet-development","tag-net","tag-net-framework"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1703","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/37781"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=1703"}],"version-history":[{"count":3,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1703\/revisions"}],"predecessor-version":[{"id":91093,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1703\/revisions\/91093"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=1703"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=1703"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=1703"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=1703"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}