What is a Sealed Class?
In C#, the sealed keyword indicates that a class cannot be inherited.
This can be used to prevent incorrect extensions when other developers use my class, especially in frameworks.
However, the .NET design guidelines recommend not to overuse sealed for the sake of extensibility. It should be used sparingly and only for types that are at risk of causing issues or are treated like values.
.NET Compiler Optimizations
The .NET compiler includes various optimizations, some of which are only applied when a class is sealed.
When a virtual method is called, the virtual method table (vtable) is consulted to find the actual method of the type.
However, for a sealed class, since it is "certain that there are no derived types," the compiler can skip the vtable lookup and call the method directly.
In cases where additional type checks, such as array covariance checks, are needed, sealed classes can also skip these checks, resulting in slightly better performance.
.NET 6 Benchmark
The results measured with .NET 6, as shown in Performance benefits of sealed class in .NET - Meziantou's blog, indicate that using sealed significantly improves performance.
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?"
.NET programs are first compiled into IL (Intermediate Language) and then translated into machine code at runtime by the JIT (Just-In-Time) compiler.
From the JIT's perspective, it cannot be sure that a class truly has no inheritance without examining the entire code.
Therefore, it is challenging to create code that completely skips the vtable.
Solutions
Option 1 - Use AOT
AOT (Ahead-of-Time) compilation converts the entire code into machine code before execution.
At this point, the compiler can understand all type relationships, allowing for aggressive optimizations of classes without inheritance.
However, AOT deployment has limitations and may not be applicable to all projects.
Option 2 - Use PGO
Starting from .NET 8, PGO (Profile-Guided Optimization) is enabled by default.
This method profiles the application during execution and compiles optimized code in real-time.
One of the optimizations performed by PGO is Devirtualization.
This converts virtual calls that are expected to have no further inheritance into direct calls.
When virtualization is removed, vtable lookups disappear, and opportunities for inlining increase, enhancing performance.
However, since it cannot be guaranteed that there is no further inheritance, runtime type checks like Guarded Devirtualization are used as a safeguard.
Thus, while it may not be as fast as direct calls, it still generates faster code than virtual calls.
.NET 6 vs .NET 10 Benchmark
Below are the results measured with the same code in .NET 6 and the latest .NET 10.
In .NET 10, the virtual method calls for NonSealed have also improved significantly, but if the class is called frequently, it may still be beneficial to explicitly use the sealed keyword.
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++;
}
Conclusion
- sealed helps improve performance by eliminating vtable lookups and type checks.
- Thanks to PGO, non-sealed classes have also become faster, but when virtual method calls are frequent, it is still worth considering the use of the sealed keyword.