Understanding C #’s Stack Memory for Better Performance

Basic Concepts:

Efficient memory management is critical to developing high-performance and reliable code in C# and other languages with a garbage collector (GC).

Showing the structure of Memory in a computer’s RAM (Random Access Memory)
Source: Dawid ”DeVinczi” Słysz/Author

Resource management might not be a primary concern in many web application development situations. Nevertheless, there are moments when client demands revolve around enhancing the speed of particular application segments. In these circumstances, skillfully utilizing stack memory can substantially reduce execution time and memory consumption to a minimum. It will be highly beneficial for both sides.

Let’s go to the total basis of programming.
To understand this, it’s essential to know that we can use three primary types of Memory to allocate in .NET.

  • Stack memory: Resides in the Stack and is allocated using the stackalloc keyword
  • Managed Memory: Resides in the heap and is managed by the GC
  • Unmanaged Memory: Resides in the unmanaged heap and is given by calling the Marshal.AllocHGlobal or Marshal.AllocCoTaskMem methods

Today’s keywords to understand:

  • stackalloc
  • Span
  • Memory
  • The stackalloc keyword:
    • Allocates Memory on the Stack
      (creates a region of Memory on the Stack and returns a pointer to the start of that Memory)
    • Automatically deallocated when the method exits scope (we can’t explicitly free the Memory allocated with stackalloc)
    • Useful in scenarios where you need to allocate a large number of small objects (allocating objects on the heap can be slower due to the overhead of garbage collection)
      can’t be used in an async method, and lambda expressions
    • Depends on system architecture, but the default stack size (per Thread) is 1MB.
  • Span and ReadOnlySpan types:
    • Defined as reference structs (only can be stored on the Stack)
    • Fully type-safe and memory-safe
    • Supports any kind of Memory
    • Low or zero overhead 🙂
    • It cannot be boxed
    • Read-only if you choose it

Example with benchmark:

Methods I used for the cases above:

Memory benchmark – memory allocations
Source: Dawid ”DeVinczi” Słysz /Author

Look at the “ConvertingToSpan” case. It did not happen accidentally. As you can see allocation occurred even when we used Span. You will probably ask how it is possible. Spans are value type! Yes, but we used a “new” keyword to create an array -> new byte[1024]. And now, the Span buffer is allocated on the stack, but it references our byte array allocated on the heap.

Let’s examine the “UsingMemoryPool” case, which can be highly advantageous when discussing memory allocations. It allows you to manage a contiguous memory block on the heap using a memory pool, resulting in fewer allocation operations. You can also use Span’s features to operate on that adjacent memory block.

You may ask why I want to use that. It’s 4 times slower than the other ones! All right, you won’t need to know that structure in about 90% of situations. However, in other cases, like when you want to efficiently manage memory allocation and optimize the execution time for processing large objects on the heap, understanding this structure becomes valuable. You can achieve this optimization by using unmanaged Memory or by pinning your object (so the garbage collector won’t automatically clean it up after execution, which you can do using GCHandle.Alloc()), and then effectively reuse it.

  • Memory and Read-Only Memory types:
    • defined as a struct (can be placed on the stack and on heap -> boxing (warning!))
    • owner/consumer model
    • can be used with async methods
    • can be used as a field in class
    • easy to work with large data structures
    • provides concurrency (when multiple threads need to access the same memory region safely)
    • useful in complex solutions

Memory is helpful in C# for efficient and safe memory manipulation. This Memory might not belong to your application process, as it could have been allocated in unmanaged code. The primary benefit of memory is that it enables you to work with data in non-contiguous buffers without unnecessary copying as if they were a single contiguous buffer.
An example benchmark is shown in Figure 2.

FAQ:

  • How about StackOverflowException in stackalloc?

You can call this method before your method with stackalloc execution. var isEnoughSpace = RuntimeHelpers.TryEnsureSufficientExecutionStack(); “This method ensures sufficient stack to execute the average Framework function.” Looks a little suspicious, but this quote is from Microsoft documentation :].

  • Can Memory cause some damage in the application?

Since Memory is a view into existing memory, you need to be careful about the lifetime of the memory it references. Using a Memory after the underlying data has been disposed of or deallocated can result in undefined damaging behavior, primarily when pointing to arrays or similar structs.

Tags: , , , , ,