In this blog entry, we would like to discuss some significant changes that were made in the .NET 4.6.x garbage collector (GC). We encourage you to get the latest version, 4.6.2.
Our main GC developer, Maoni Stephens wrote up a description of the enhancements that were delivered with the 4.6.2 framework. These changes were made in order to improve performance of the framework and to allow the garbage collector to operator more efficiently.
Make the Pinning Promotion More Efficient
In previous versions of the .NET Framework when an object reported as pinned survived, we couldn’t move that object and the adjacent live objects. Since we tend to leave pins in ephemeral generations (so you can use the free spaces between them sooner), this means we needed to scan them when performing ephemeral collections. If many other objects right around the pinned objects survived collection also then the ephemeral collection time could be higher than desired. Starting with version 4.6.2, this limitation was lifted so we could compact away the adjacent live objects around pinned objects. In testing, we saw dramatic improvements of ephemeral collection time in scenarios where GC was artificially pinning a lot of objects.
In the Before pic, the 2 non pinned objects get compacted away but the non pinned object next to the pinned object stays in gen0.
In 4.6.x this limitation was lifted so we could compact away the adjacent live objects around them. We saw dramatic improvements of ephemeral collection time in scenarios where GC was artificially pinning a lot of objects. In the After pic, we can see that the non pinned object next to pinned gets compacted away:
When you pin a lot, such as when IO operations last for a long time and you observe long ephemeral GC pauses, you would very likely benefit from this change.
Using Free Space in Gen2 More Efficiently
In prior versions of the framework we used a first fit approach when we were compacting gen1 survivors into gen2 free list, which meant the GC discarded spaces that could not be used. This technique led to wasted spaces in memory.
In the Before pic, we see that F0 disappeared because it was too small to fit S. We were able to compact S into F1 and part of F1 remains (for further compaction from gen1).
To improve on this and utilize memory more efficiently, we introduced a bucketed free list where we threaded left over free spaces into their respective bucket. We had to be very careful of the usage in the smallest bucket as there could be many of them. We also worked on a policy of which bucket to try as we didn’t want to lengthen the gen1 collection time much by spending more time in searching for a usable free space. We want to stay doing background GCs as much as possible in order to be more efficient at consuming free spaces.
In the After pic F0 remains on the free list and can be used for further compaction from gen1. And we try to fit S into F1 right away.
If you observe your gen2 memory increasing, one reason could be that we ran out of items on the free list. Consequently, promoting gen1 survivors would require an increase in gen2 size. For scenarios that are helped by this change you would see gen2 size increase much less quickly and we can accommodate more gen1 collections for every gen2 collection. This also means you would see blocking gen2s triggered less frequently because when the heap gets too big which causes the memory load to be too high we would do a blocking gen2 to compact. In our tests, we have observed first party scenarios where the ratio of gen1 collections to gen2 collections increased from 20 to 1 to more than 200 gen1 collections for every gen2 collection.
If you were running into situations described above, you should definitely try out .NET 4.6.2 to benefit from these enhancements in the garbage collector.