Performance Considerations of .NET Array Covariance
This content was translated from Korean using AI.

What is Covariance?

Covariance might sound like a complex term, but it simply means 'the property of being able to change together.'

For example, when a type A can be changed to type B, the derived type C<A> can also change to C<B>. This is what we refer to as covariance.

A common example of covariance in C# is IEnumerable\<T\>.

IEnumerable<string> strings = new List<string>();

// When string can be changed to object,
// IEnumerable<string> can also be changed to IEnumerable<object>
IEnumerable<Object> objects = strings;

Runtime Errors Due to Covariance

Another example of covariance in C# is arrays.

string[] strArray = new string[5];

// string[] can be converted to object[]
object[] objArray = strArray;

But what happens if we try to insert an instance of a class that is not a string into the object array? .NET checks at runtime whether an unsafe type is being inserted into the array, resulting in an ArrayTypeMismatchException.

object[] objArray = new string[5];

// System.ArrayTypeMismatchException:
// 'Attempted to access an element as a type incompatible with the array.'
objArray[0] = 1234;

Avoiding Type Check Costs (Stelem_Ref)

When .NET stores elements in an array, it means that it incurs additional costs to check for safety.

In general logic, this overhead is small enough to be negligible, and if the value being stored is null or the same as the array's value, optimizations are performed to skip the check since the value is already validated (JIT: avoid store covariance check for ref-type ldelem from same array · Issue #9159 · dotnet/runtime).

However, if this code is executed frequently in a hot path, it may require attention.

If profiling reveals this overhead, it will appear under the name Stelem_Ref.

Option 1 - Use the sealed Keyword

As discussed in the article .NET Sealed Class Performance Considerations, the compiler can be sure that no further inheritance occurs with a sealed class.

Thus, if you apply the sealed keyword to the class that is the target of the array, the compiler can confidently optimize away the type check, knowing that the type being stored is not actually another type (subclass).

sealed class MyClass { }

// No array type check when storing in the array
MyClass[] myArray = new MyClass[5];
myArray[0] = new MyClass();

Option 2 - Direct Memory Writing

In situations where unsafe code is allowed and you can be sure of the variable's type, you can bypass type checking and write directly to memory.

Having too much fixed memory can impact GC performance, and if an incorrect type is inserted, it can lead to runtime errors elsewhere, so caution is advised when using values stored in the array.

public static void SetArrayValue<T>(T[] array, int index, T value)
{
	unsafe
	{
		fixed (T* _ = array) // Fix to prevent GC from moving it
		{
			ref T start = ref MemoryMarshal.GetArrayDataReference(array);
			Unsafe.Add(ref start, index) = value;
		}
	}
}

object[] objArray = new string[5];

SetArrayValue(objArray, 0, 4); // No runtime error
SetArrayValue(objArray, 1, "test");

Option 3 - Use Struct Wrappers

Option 1 can only be used when directly defining the target of the array, relying on uncertain compiler optimizations. Option 2 uses unsafe code and affects GC.

A better approach is to wrap the array in a struct, as structs are value types and therefore do not undergo covariance type checks. Like option 2, this also avoids type checks, which means that incorrect types can still be inserted, so caution is still necessary when using values stored in the array.

class MyClass { }
class MyChildClass : MyClass { }

struct Element
{
	public MyClass Value;
}

Element[] elementArray = new Element[5];
elementArray[0] = new Element { Value = new MyClass() };
elementArray[1] = new Element { Value = new MyChildClass() }; // No runtime error

In particular, object pools often use struct wrappers because the class of the stored objects is defined by the user.

A notable example is the ObjectPool implementation in Roslyn: