Reduce memory allocations using new C# features
The techniques described in this section improve performance when applied to hot paths in your code. Hot paths are those sections of your codebase that are executed often and repeatedly in normal operations. Applying these techniques to code that isn't often executed will have minimal impact. Before making any changes to improve performance, it's critical to measure a baseline. Then, analyze that baseline to determine where memory bottlenecks occur. You can learn about many cross platform tools to measure your application's performance in the section on Diagnostics and instrumentation. You can practice a profiling session in the tutorial to Measure memory usage in the Visual Studio documentation.
Once you've measured memory usage and have determined that you can reduce allocations, use the techniques in this section to reduce allocations. After each successive change, measure memory usage again. Make sure each change has a positive impact on the memory usage in your application.
Performance work in .NET often means removing allocations from your code. Every block of memory you allocate must eventually be freed. Fewer allocations reduce time spent in garbage collection. It allows for more predictable execution time by removing garbage collections from specific code paths.
A common tactic to reduce allocations is to change critical data structures from
class types to
struct types. This change impacts the semantics of using those types. Parameters and returns are now passed by value instead of by reference. The cost of copying a value is negligible if the types are small, three words or less (considering one word being of natural size of one integer). It's measurable and can have real performance impact for larger types. To combat the effect of copying, developers can pass these types by
ref to get back the intended semantics.
ref features give you the ability to express the desired semantics for
struct types without negatively impacting their overall usability. Prior to these enhancements, developers needed to resort to
unsafe constructs with pointers and raw memory to achieve the same performance impact. The compiler generates verifiably safe code for the new
ref related features. Verifiably safe code means the compiler detects possible buffer overruns or accessing unallocated or freed memory. The compiler detects and prevents some errors.
Pass and return by reference
Variables in C# store values. In
struct types, the value is the contents of an instance of the type. In
class types, the value is a reference to a block of memory that stores an instance of the type. Adding the
ref modifier means that the variable stores the reference to the value. In
struct types, the reference points to the storage containing the value. In
class types, the reference points to the storage containing the reference to the block of memory.
In C#, parameters to methods are passed by value, and return values are return by value. The value of the argument is passed to the method. The value of the return argument is the return value.
ref readonly, or
out modifier indicates that the argument is passed by reference. A reference to the storage location is passed to the method. Adding
ref to the method signature means the return value is returned by reference. A reference to the storage location is the return value.
You can also use ref assignment to have a variable refer to another variable. A typical assignment copies the value of the right hand side to the variable on the left hand side of the assignment. A ref assignment copies the memory location of the variable on the right hand side to the variable on the left hand side. The
ref now refers to the original variable:
int anInteger = 42; // assignment.
ref int location = ref anInteger; // ref assignment.
ref int sameLocation = ref location; // ref assignment
Console.WriteLine(location); // output: 42
sameLocation = 19; // assignment
Console.WriteLine(anInteger); // output: 19
When you assign a variable, you change its value. When you ref assign a variable, you change what it refers to.
You can work directly with the storage for values using
ref variables, pass by reference, and ref assignment. Scope rules enforced by the compiler ensure safety when working directly with storage.
ref readonly and
in modifiers both indicate that the argument should be passed by reference and can't be reassigned in the method. The difference is that
ref readonly indicates that the method uses the parameter as a variable. The method might capture the parameter, or it might return the parameter by readonly reference. In those cases, you should use the
ref readonly modifier. Otherwise, the
in modifier offers more flexibility. You don't need to add the
in modifier to an argument for an
in parameter, so you can update existing API signatures safely using the
in modifier. The compiler issues a warning if you don't add either the
in modifier to an argument for a
ref readonly parameter.
Ref safe context
C# includes rules for
ref expressions to ensure that a
ref expression can't be accessed where the storage it refers to is no longer valid. Consider the following example:
public ref int CantEscape()
int index = 42;
return ref index; // Error: index's ref safe context is the body of CantEscape
The compiler reports an error because you can't return a reference to a local variable from a method. The caller can't access the storage being referred to. The ref safe context defines the scope in which a
ref expression is safe to access or modify. The following table lists the ref safe contexts for variable types.
ref fields can't be declared in a
class or a non-ref
struct, so those rows aren't in the table:
|ref safe context
|block where local is declared
ref field of
A variable can be
ref returned if its ref safe context is the calling method. If its ref safe context is the current method or a block,
ref return is disallowed. The following snippet shows two examples. A member field can be accessed from the scope calling a method, so a class or struct field's ref safe context is the calling method. The ref safe context for a parameter with the
in modifiers is the entire method. Both can be
ref returned from a member method:
private int anIndex;
public ref int RetrieveIndexRef()
return ref anIndex;
public ref int RefMin(ref int left, ref int right)
if (left < right)
return ref left;
return ref right;
ref readonly or
in modifier is applied to a parameter, that parameter can be returned by
ref readonly, not
The compiler ensures that a reference can't escape its ref safe context. You can use
ref return, and
ref local variables safely because the compiler detects if you've accidentally written code where a
ref expression could be accessed when its storage isn't valid.
Safe context and ref structs
ref struct types require more rules to ensure they can be used safely. A
ref struct type can include
ref fields. That requires the introduction of a safe context. For most types, the safe context is the calling method. In other words, a value that's not a
ref struct can always be returned from a method.
Informally, the safe context for a
ref struct is the scope where all of its
ref fields can be accessed. In other words, it's the intersection of the ref safe context of all its
ref fields. The following method returns a
ReadOnlySpan<char> to a member field, so its safe context is the method:
private string longMessage = "This is a long message";
public ReadOnlySpan<char> Safe()
var span = longMessage.AsSpan();
In contrast, the following code emits an error because the
ref field member of the
Span<int> refers to the stack allocated array of integers. It can't escape the method:
public Span<int> M()
int length = 3;
Span<int> numbers = stackalloc int[length];
for (var i = 0; i < length; i++)
numbers[i] = i;
return numbers; // Error! numbers can't escape this method.
Unify memory types
The introduction of System.Span<T> and System.Memory<T> provide a unified model for working with memory. System.ReadOnlySpan<T> and System.ReadOnlyMemory<T> provide readonly versions for accessing memory. They all provide an abstraction over a block of memory storing an array of similar elements. The difference is that
ref struct types whereas
struct types. Spans contain a
ref field. Therefore instances of a span can't leave its safe context. The safe context of a
ref struct is the ref safe context of its
ref field. The implementation of
ReadOnlyMemory<T> remove this restriction. You use these types to directly access memory buffers.
Improve performance with ref safety
Using these features to improve performance involves these tasks:
- Avoid allocations: When you change a type from a
struct, you change how it's stored. Local variables are stored on the stack. Members are stored inline when the container object is allocated. This change means fewer allocations and that decreases the work the garbage collector does. It might also decrease memory pressure so the garbage collector runs less often.
- Preserve reference semantics: Changing a type from a
structchanges the semantics of passing a variable to a method. Code that modified the state of its parameters needs modification. Now that the parameter is a
struct, the method is modifying a copy of the original object. You can restore the original semantics by passing that parameter as a
refparameter. After that change, the method modifies the original
- Avoid copying data: Copying larger
structtypes can impact performance in some code paths. You can also add the
refmodifier to pass larger data structures to methods by reference instead of by value.
- Restrict modifications: When a
structtype is passed by reference, the called method could modify the state of the struct. You can replace the
refmodifier with the
inmodifiers to indicate that the argument can't be modified. Prefer
ref readonlywhen the method captures the parameter or returns it by readonly reference. You can also create
readonly structtypes or
readonlymembers to provide more control over what members of a
structcan be modified.
- Directly manipulate memory: Some algorithms are most efficient when treating data structures as a block of memory containing a sequence of elements. The
Memorytypes provide safe access to blocks of memory.
None of these techniques require
unsafe code. Used wisely, you can get performance characteristics from safe code that was previously only possible by using unsafe techniques. You can try the techniques yourself in the tutorial on reducing memory allocations.