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

What Is Covariance?

Covariance may sound like a complicated term when you first hear it, but it simply means “the ability to vary together.”

For example, if a type A can be converted to a type B, covariance allows a derived type C<A> to be converted to C<B> as well.

In C#, IEnumerable<T> is an interface that supports covariance.

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

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

Runtime Errors Caused by Covariance

Arrays are another type in C# that supports covariance.

string[] strArray = new string[5];

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

Arrays can hold values.
So what happens if you put an instance of a class other than string into an object array?

Although objArray is an object array, it is actually backed by a string array. Putting an object of a type other than string into it could cause problems.
To prevent this, .NET checks at runtime whether a value being stored in an array has a safe type. If the type is incompatible, 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 the Type Check Cost (Stelem_Ref)

The fact that .NET checks for type safety every time an element is stored in an array means there is an additional cost.

For typical code, this overhead is negligible.
.NET also applies optimizations that skip the check when the value being stored is null or comes from the same array, since it has already been validated.
(JIT: avoid store covariance check for ref-type ldelem from same array · Issue #9159 · dotnet/runtime)

However, it may be worth considering in code that runs very frequently, such as a hot path.

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

Option 1: Use the sealed Keyword

As discussed in .NET Sealed Class Performance Considerations,
the compiler can be certain that a sealed class cannot be inherited.

Therefore, if you mark the array’s element type as sealed,
the compiler can be certain that the actual array type is not a different type (a subclass),
allowing it to optimize away the type check.

sealed class MyClass { }

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

Option 2: Write Directly to Memory

If unsafe code is allowed and you are certain of the types involved,
you can bypass the type check and write directly to memory.

Pinning a lot of memory can affect GC performance,
and storing an incorrect type can cause runtime errors elsewhere, so values stored in the array must be used with care.

public static void SetArrayValue<T>(T[] array, int index, T value)
{
	unsafe
	{
		fixed (T* _ = array) // Pin the array to prevent the 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 a struct Wrapper

Option 1 is only available when you define the array’s element type yourself, and it relies on a compiler optimization that is not guaranteed.
Option 2 uses unsafe code and affects the GC.

A better approach is to wrap the elements in a struct before creating the array. Since a struct is a value type, no covariance type check is performed.
As with option 2, the check is not performed, so care is needed to ensure that values stored in the array are valid.

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

Object pools often use struct wrappers because users define the classes of the objects being stored.

A well-known example is Roslyn’s ObjectPool implementation.