When running a .NET application, the runtime allocates a big chunk of memory in which it manages (de)allocations of objects.
Allocations are done whenever a new object instance is created, deallocations are handled by the Garbage Collector (GC).
The 2 components that belong to GC are the allocator and the collector.
The allocator is responsible for getting more memory and triggering the collector when appropriate.
The collector reclaims garbage, or the memory of objects that are no longer in use by the program.
There are other ways that the collector can get called, such as manually calling GC.Collect or the finalizer thread receiving an asynchronous notification of the low memory (which triggers the collector).
Memory from the heap is allocated when you new an object.
Executing var person = new Person(); will allocate a Person object on the heap, consuming memory.
No memory is allocated when making use of value types, such as int or bool, a custom struct.
The garbage collector never has to collect these, as they are not “pointers towards a value elsewhere in memory”, like objects. They just contain the value directly.
int i = 42;
// boxing - wraps the value type in an "object box"
// (allocating a System.Object)
object o = i;
// unboxing - unpacking the "object box" into an int again
// (CPU effort to unwrap)
int j = (int)o;
There are some other interesting cases where you would not expect allocations. Let’s see how we can detect them:
Tools:
Quick note:
When using any IL viewer to look at allocations, make sure to build using the Release build configuration.
Debug builds usually do not run any/all compiler optimizations, making for different IL code from what you would see with a Release build.
Console.WriteLine(string.Concat("Answer", 42, true));
How about this line of code?
In IL, this is compiled to:
private static void ParamsArray()
{
ParamsArrayImpl();
}
private static void ParamsArrayImpl(params string[] data)
{
foreach (var x in data)
{
Console.WriteLine(x);
}
}
Working with params arrays in methods.
Have a look at this code:
There’s a hidden allocation in here…
The call to ParamsArrayImpl() looks like this in IL:
Quick note:
Starting with .NET 4.6, no empty array will be allocated. Instead, Array.Empty<T> will be passed which is a cached, empty array.
private static double AverageWithinBounds(
int[] inputs,
int min,
int max)
{
var filtered = from x in inputs
where (x >= min) && (x <= max)
select x;
return filtered.Average();
}
There are many more examples, try looking at this piece of code’s IL:
var strings = new string[] { "x", "y"};
foreach (var s in strings)
{
Task.Run(() => Console.WriteLine(s));
}
(spoiler alert: a new <>c__DisplayClass0_0 will be allocated, capturing s for use in the System.Action)
[
{
"name": "Westmalle Tripel",
"brewery": "Brouwerij der Trappisten van Westmalle",
"rating": 99.9,
"votes": 256974
},
...
]
JSON array:
Requirements:
public static void LoadBeers()
{
Beers = new Dictionary<string, Dictionary<string, double>>();
using (var reader = new JsonTextReader(new StreamReader(File.OpenRead("beers.json"))))
{
while (reader.Read())
{
if (reader.TokenType == JsonToken.StartObject)
{
// Load object from the stream
var beer = JObject.Load(reader);
var breweryName = beer.Value<string>("brewery");
var beerName = beer.Value<string>("name");
var rating = beer.Value<double>("rating");
// Add beers per brewery dictionary if it does not exist
Dictionary<string, double> beersPerBrewery;
if (!Beers.TryGetValue(breweryName, out beersPerBrewery))
{
beersPerBrewery = new Dictionary<string, double>();
Beers.Add(breweryName, beersPerBrewery);
}
// Add beer
if (!beersPerBrewery.ContainsKey(beerName))
{
beersPerBrewery.Add(beerName, rating);
}
}
}
}
}
for (var i = 0; i < 10; i++)
{
BeerLoader.LoadBeers();
Console.ReadLine();
}
Short answer:
.NET already have such a feature and that’s called the NoGCRegion.
GC.TryStartNoGCRegion API allows you to tell us the amount of allocations you’d like to do and when you stay within it, no GCs will be triggered.
GC.EndNoGCRegion will revert back to doing normal GCs
Long answer:
There’s currently limitations on how much you can allocate with NoGCRegion
If you are using Server GC you are able to ask for a lot more memory on SOH because its SOH segment size is a lot larger.
Currently (and this has been the case for a long time) the default seg size on 64-bit for Server GC is 4GB
When you have > 4 procs it means the SOH segment size is 1GB each.
If have > 8 procs it means the SOH segment size is 1GB each.
On Desktop you have ways to make the SOH segment size larger –
use the gcSegmentSize in app config
There is an old adage in IT that says “don’t do premature optimization”.
In other words: maybe some allocations are okay to have, as the GC will take care of cleaning them up anyway.