Performance Considerations for Sealed Classes in .NET
This content was translated from Korean using AI.

What Is a Sealed Class?

C# has a sealed keyword that indicates a class cannot be inherited.
When developing a framework, for example, you can use sealed to prevent inheritance and stop other developers from extending a class incorrectly.

However, the .NET design guidelines recommend avoiding excessive use of sealed to preserve extensibility.
Instead, they suggest using it selectively for types where inheritance poses a risk or that behave like values.

.NET Compilation Optimizations

The .NET compiler performs various optimizations, some of which apply only when a class is sealed.

Normally, calling a virtual method requires looking up the virtual method table (vtable) to find the method for the actual type.
But with a sealed class, the compiler knows for certain that there are no further derived types, so it can skip the vtable lookup and call the method directly.

Beyond virtual method calls, sealed classes can also improve performance slightly in cases that require additional type checks, such as array covariance checks.
Because the class cannot be inherited, some of those checks can be omitted.

.NET 6 Benchmark

The following blog post profiles the performance of sealed classes in .NET 6:
Performance benefits of sealed class in .NET - Meziantou's blog

As you can see, the sealed version performs better.

Method Mean Error StdDev
NonSealed 0.4465 ns 0.0276 ns 0.0258 ns
Sealed 0.0107 ns 0.0160 ns 0.0150 ns

Can't the Compiler Optimize This Automatically?

A .NET program is first compiled into IL (Intermediate Language), which the JIT compiler translates into machine code at runtime.

The JIT cannot see all the code, so it cannot be certain that a class has no derived types.
As a result, it cannot always fully optimize these calls at runtime.

Optimization Options

Option 1: Use AOT

AOT (Ahead-of-Time) compilation converts the entire application into machine code before execution.
The compiler can then see all type relationships and aggressively optimize classes that have no derived types.

However, AOT deployment has limitations and isn't suitable for every project.

Option 2: Use PGO

Starting with .NET 8, PGO (Profile-Guided Optimization) is enabled by default.
It profiles the application as it runs and compiles optimized code in real time.

One of the optimizations PGO performs is devirtualization.
It can turn a virtual call into a direct call when it expects there to be no further derived types.
Devirtualization eliminates the vtable lookup and creates more opportunities for inlining, improving performance.

However, because it cannot be certain that there are no further derived types, it uses runtime type checks as a safeguard, as in guarded devirtualization.
The resulting code isn't as fast as an unconditional direct call, but it can still be faster than a virtual call.

Option 3: Use sealed

Performance benefits of sealed class in .NET - Meziantou's blog

As suggested in the blog post above, you can use the Meziantou.Analyzer package to easily find classes that can be sealed.
You can then seal classes whose virtual methods are expected to be called frequently on a hot path.

.NET 6 vs. .NET 10 Benchmark

Below are results measured using the same code on .NET 6 and the latest version, .NET 10.

In .NET 10, virtual method calls on NonSealed have become considerably faster,
but explicitly marking a class as sealed may still be beneficial when its methods are called frequently.

Method Runtime Mean Error StdDev
NonSealed .NET 6.0 115.06 us 2.299 us 3.369 us
Sealed .NET 6.0 28.83 us 0.549 us 0.513 us
NonSealed .NET 10.0 25.44 us 0.503 us 0.826 us
Sealed .NET 10.0 23.52 us 0.415 us 0.368 us
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net10_0)]
public class SealedCall
{
	readonly NonSealedType nonSealedType = new();
	readonly SealedType sealedType = new();

	const int TestNum = 100000;

	[Benchmark]
	public long NonSealed()
	{
		nonSealedType.num = 0;
		for (int i = 0; i < TestNum; i++)
		{
			nonSealedType.Method();
		}
		return nonSealedType.num;
	}

	[Benchmark]
	public long Sealed()
	{
		sealedType.num = 0;
		for (int i = 0; i < TestNum; i++)
		{
			sealedType.Method();
		}
		return sealedType.num;
	}
}

internal class BaseType
{
	public long num = 0;
	public virtual void Method() { }
}

internal class NonSealedType : BaseType
{
	public override void Method() => num++;
}

internal sealed class SealedType : BaseType
{
	public override void Method() => num++;
}

Summary

  • sealed can improve performance by eliminating vtable lookups and type checks.
  • PGO has made non-sealed classes faster too, but using the sealed keyword is still worth considering when virtual methods are called frequently.