.Net — Memory Management (Arrays)

Hari Prasad
ReDive
Published in
4 min readNov 14, 2021

If you use serialization and de-serialization of arrays (or) re-creating arrays multiple times in your application (or) use arrays while receiving data from Serial Com / Ethernet by listening to receiving events, then this article is for you!

We use arrays when we need to store primitive list of data, while transferring data from source to destination, to compute a series of operations, sorting and merging of data. But it has its own cons of memory utilization and performance deprivation if not used efficiently. Arrays are referenced types. While creating an array, CLR allocates a series of memory objects which will be traversed to access individual memory objects.

Lets look how GC impacts our array creations. We know that GC maintains a heap of memory objects in 3 generations. But every time when the memory object reaches Gen 0 and is not currently in use within the application, then the memory object will be garbage collected, leading to utilize some time to collect those objects.

Performance

While GC starts to collect the memory objects, then it will pause the application thread for a certain amount of time (usually in milliseconds). Consider your application creates new array instances for every API request to process your inputs or to return array of data objects. For e.g. the API receives 100k requests at a time, so it creates 100k instances, and when the API request gets completed they were sequentially garbage collected. So when the GC happens to collect the unused memory (consider GC collects after serving every 100 requests), then the API will see some reduction in performance (API will be paused for every 100 requests served).

Memory Utilization

Each time the app creates a new array instance, it allocates a predetermined amount of memory based on the size of the array. Consider in the above example, the API creates 100k array instances. Memory will be allocated proportionately based on the size of the array of each instance. If the API runs in low memory conditions, then receiving more requests to it will in-turn creates absolute sizes of memory, utilizing more memory which might be needed for other object allocations, and might lead to Out of memory condition.

Analysis with PerfView

To explain the downsides, I have created a console application, which will create byte[] of size 255 and loops it for 1000k times. Refer the below GitHub page for viewing the complete application.

Method SimulateCreationOfByteArrays will create new byte[] of provided length and loops it for supplied executionCount.

for(int i = 0; i < executionCount; i++){  CreateByteArrays(length);  LoadByteArray(_newlyCreatedByteArray);}private void CreateByteArrays(int length){     _newlyCreatedByteArray = new byte[length];}

I have used PerfView the high speed performance analyzer tool which uses Event tracing for Windows (ETW). It can be downloaded from GitHub. And you can find several videos in Youtube talking about how to use this tool, as well as in-built documentation in same tool.

GC Heap alloc for Array initializations

The above image shows the heap allocations made by creating 1000k array instances each of size 255.

It leads to allocation of 544 MB which is huge !

It also provides below GCStats (This includes generation of both Array and ArrayPool explained in next header)

  • Total CPU Time: 2,704 msec
  • Total GC CPU Time: 15 msec
  • Total Allocs : 557.875 MB
  • GC CPU MSec/MB Alloc : 0.027 MSec/MB
  • Total GC Pause: 81.7 msec
  • % Time paused for Garbage Collection: 0.6%
  • % CPU Time spent Garbage Collecting: 0.6%

Efficient Solution

To serve the above explained purpose, .Net provides ArrayPool<T> which is present in System.Buffers namespace. ArrayPool<T> is inherited from System.Object. Being an abstract class, it provides a shared singleton instance to use its base methods to retrieve (Rent) and release (Return) the array instances from an existing pool. Array pool instance is created only once by using Create method

ArrayPool<T>.Create (or) ArrayPool<T>.Create(int, int) 

Both methods create a singleton instance which can be accessed by Shared property.

ArrayPool<T>.Shared

From same above GitHub example, the method named SimulateCreationOfByteArrayPool will be creating the pooled instance, Rents the Array of specified size, Loads the array and Returns the array back to the ArrayPool. The same Rent/Load/Return is looped for give executionCount times.

CreateByteArrayPool(length);for (int i = 0; i < executionCount; i++){     RentByteArrayPool(length);     LoadByteArray(_pooledByteArray);     ReleaseByteArrayPool(_pooledByteArray);}
------------------------------------------------------------
private void CreateByteArrayPool(int length){ byteArrayPool = ArrayPool<byte>.Create(length, maxArraysPerBucket);}private void RentByteArrayPool(int length){ _pooledByteArray = byteArrayPool.Rent(length);}
private void ReleaseByteArrayPool(byte[] aByte){
byteArrayPool.Return(aByte, true);}

Lets look at the PerfView GC heap alloc for the above code.

GC Heap alloc for ArrayPool

The above image shows that the memory allocation is done only once, during the Pool creation.

This leads to allocation of only 1 MB of memory !

Which is a significant reduction from 554MB in previous case which also impacted performance by GC collection.

--

--

Hari Prasad
ReDive
Editor for

Hari is a software professional with an experience of morethan 13 years in software industry. Wishes to be updated in latest technological trends.