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

What is Covariance?

Covariance might sound like a complex term at first, but it simply means the property of being able to change along with something else.

For example, if type A can be changed to type B, then a derived type C can also change to C. This is what we refer to as covariance.

In C#, one class that exhibits covariance is IEnumerable.

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 class in C# that exhibits covariance is arrays.

string[] strArray = new string[5];

// string[] -> object[]
object[] objArray = strArray;

Arrays are classes that can hold values. So what happens if we try to insert an instance of a class that is not a string into an object array?

Although objArray is an object array, it is actually a string array. Therefore, inserting an object of a type other than string could lead to issues later on. To prevent this, .NET checks at runtime whether an unsafe type is being inserted into the array, and if it finds a mismatch, it throws 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)

The fact that .NET checks whether elements can be safely stored in an array implies that there is an additional cost involved.

In most cases, this overhead is negligible, and if the value being stored is null or the same as an already validated value in the array, optimizations are in place to skip the check (see JIT: avoid store covariance check for ref-type ldelem from same array · Issue #9159 · dotnet/runtime).

However, if you are dealing with a hot path—code that runs very frequently—you may need to pay attention to this.

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

Option 1 - Use the sealed Keyword

As discussed in the article on .NET Sealed Class Performance Considerations, the compiler can be certain that a sealed class will not be inherited any further.

By marking the class that is the target of the array as sealed, the compiler can be confident that the type being stored is not actually a different type (a subclass), allowing it to optimize by skipping the type check.

sealed class MyClass { }

// No type check on array storage
MyClass[] myArray = new MyClass[5];
myArray[0] = new MyClass();

Option 2 - Direct Memory Writing

If unsafe code is permitted and you can be certain of the type of the given variable, you can bypass type checks and write directly to memory.

Having a lot of fixed memory can impact GC performance, and if an incorrect type is inserted, it may 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 - Using Struct Wrappers

Option 1 is only applicable 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 data in a struct to create the array. Since structs are value types, they do not undergo covariance type checks. Like option 2, this also means that type checks are not performed, 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

This struct wrapper approach is often used in object pools, where the class of the stored object is defined by the user.

A notable example is the ObjectPool implementation in Roslyn.