Why & When Sealed Keyword could lead to a performance boost in .NET C#
What does it mean to write compiler-friendly code?
Any .NET code passes by more than one phase till finally reaches the machine code. Since many factors are involved in this process, there could be lots of details that we miss when we are first writing our code.
However, the more clear and deterministic the code we write is, the more the compiler could assist us and generate optimized machine code.
In this article, we are going to discuss one example of the ways by which we can help the compiler optimize our code. This way is; using the Sealed Keyword.
Enough with the talking and let’s see an example…
Background Check
If you are a .NET developer, even a beginner, you should know by now that there is a keyword in the .NET framework called sealed.
This keyword could be used in a class definition and then it means that the class could not be inherited by any other classes. It looks like this:
public sealed class MyClass {}
Or even in a method declaration and then it means that the method could not be overridden -anymore- by any other method in a child class. In other words, it breaks the method override series at the level where it is used. It looks like this:
public sealed override void MyMethod() {}
Therefore, what we can understand from this is that when we use the sealed keyword, we are actually promising the compiler that we don’t have any intentions to inherit from a class or override a method.
Having said that, let’s now see if that would mean anything to the compiler.
Let’s start with some base code to be used throughout the explanation.
BaseClass
public class BaseClass
{
public virtual void DoSomething()
{
}
public void DoSomethingElse()
{
}
}
This is the base class that we would use as the top parent.
In this class, we have the following members defined:
public virtual void DoSomething() method.
public void DoSomethingElse() method.
MyClass
public class MyClass : BaseClass
{
public override void DoSomething()
{
}
}
This is the class that would inherit from BaseClass but without using the sealed keyword in the definition.
In this class, we are overriding DoSomething method which is inherited from the parent BaseClass class.
MySealedClass
public sealed class MySealedClass : BaseClass
{
public override void DoSomething()
{
}
}
This is the class that would inherit from BaseClass but this time we are using the sealed keyword in the definition.
In this class, we are overriding DoSomething method which is inherited from the parent BaseClass class.
Now, let’s proceed and see if there would be any difference -from the compiler’s point of view- between using MyClass and MySealedClass classes.
Calling Virtual Method
To validate if there would be any difference -from the compiler's point of view- between calling a virtual method on both MyClass and MySealedClass classes, we would create a Benchmark project.
[MemoryDiagnoser(false)]
public class Benchmarking
{
private readonly int NumberOfTrials = 10;
private MyClass _myClassObject = new MyClass();
private MySealedClass _mySealedClassObject = new MySealedClass();
[Benchmark]
public void CallingVirtualMethodOnMyClass()
{
for (var i = 0; i < NumberOfTrials; i++)
{
_myClassObject.DoSomething();
}
}
[Benchmark]
public void CallingVirtualMethodOnMySealedClass()
{
for (var i = 0; i < NumberOfTrials; i++)
{
_mySealedClassObject.DoSomething();
}
}
}
Now, running this Benchmark project we would get the following result.
As we can notice here, the performance of calling the virtual method on the sealed class is much better than calling it on the non-sealed class.
But why?!!! Let me tell you.
On Non-Sealed Class
While calling the virtual method on the object created from MyClass class, at that moment the compiler doesn’t know if there is some code that reinitialized the _myClassObject object with a new instance of a child class of MyClass class or not. This assumption is valid because MyClass class is not sealed and this means that it could be inherited.
Based on that assumption, the compiler could not decide -at compile time- if the actual implementation of the DoSomething method would be the one provided by MyClass class or any other child class of it.
Thus, the compiler would write some instructions -to be executed at runtime- to check at the moment of executing the DoSomething method, which implementation would be the right one. This for sure would cost more processing and time.
Note: as you noticed the compiler would suspect that some code could reinitialize the object. You might think that marking the field as readonly would solve the problem but actually no as still the object could be reinitialized inside the constructor.
On Sealed Class
While calling the virtual method on the object created from MySealedClass class, at that moment the compiler doesn’t know if there is some code that reinitialized the _mySealedClassObject object with a new instance or not. However, the compiler is sure that even if this happened, the instance would still be of MySealedClass class as it is sealed and this means that it would never have any child classes.
Based on that, the compiler would decide -at compile time- the actual implementation of the DoSomething method. This is for sure much faster than waiting for the runtime.
Calling Non-Virtual Method
To validate if there would be any difference -from the compiler's point of view- between calling a non-virtual method on both MyClass and MySealedClass classes, we would create a Benchmark project.
[MemoryDiagnoser(false)]
public class Benchmarking
{
private readonly int NumberOfTrials = 10;
private BaseClass _baseClassObject = new BaseClass();
private MyClass _myClassObject = new MyClass();
private MySealedClass _mySealedClassObject = new MySealedClass();
private MyClass[] _myClassObjectsArray = new MyClass[1];
private MySealedClass[] _mySealedClassObjectsArray = new MySealedClass[1];
[Benchmark]
public void CallingNonVirtualMethodOnMyClass()
{
for (var i = 0; i < NumberOfTrials; i++)
{
_myClassObject.DoSomethingElse();
}
}
[Benchmark]
public void CallingNonVirtualMethodOnMySealedClass()
{
for (var i = 0; i < NumberOfTrials; i++)
{
_mySealedClassObject.DoSomethingElse();
}
}
}
Now, running this Benchmark project we would get the following result.
As we can notice here, the performance of calling the non-virtual method on the sealed class is better than calling it on the non-sealed class.
However, there is no scientific evidence on why this is happening and actually running the same benchmark project again might get the opposite results.
Therefore, most probably this difference is caused by the benchmarking framework itself as the difference is too small that it could be negligible.
Type Checking
To validate if there would be any difference -from the compiler's point of view- between checking the type of an object using the is operator on both MyClass and MySealedClass classes, we would create a Benchmark project.
[MemoryDiagnoser(false)]
public class Benchmarking
{
private readonly int NumberOfTrials = 10;
private BaseClass _baseClassObject = new BaseClass();
[Benchmark]
public bool ObjectTypeIsMyClass()
{
for (var i = 0; i < NumberOfTrials; i++)
{
var x = _baseClassObject is MyClass;
}
return true;
}
[Benchmark]
public bool ObjectTypeIsMySealedClass()
{
for (var i = 0; i < NumberOfTrials; i++)
{
var x = _baseClassObject is MySealedClass;
}
return true;
}
}
Now, running this Benchmark project we would get the following result.
As we can notice here, the performance of checking the object type on the sealed class is better than calling it on the non-sealed class.
But why?!!! Let me tell you.
On Non-Sealed Class
While checking if the type of the object is MyClass class, the compiler needs to check if the object is of the type MyClass class or any of its child classes.
Thus, this leads to more instructions and for sure more processing and time.
On Sealed Class
While checking if the type of the object is MySealedClass class, the compiler needs to check if the object is only of the type MySealedClass class, nothing else. This is because MySealedClass class is sealed and this means that it would never have any child classes.
Thus, this leads to fewer instructions and for sure less processing and time.
Type Casting
To validate if there would be any difference -from the compiler's point of view- between casting an object using the as operator on both MyClass and MySealedClass classes, we would create a Benchmark project.
[MemoryDiagnoser(false)]
public class Benchmarking
{
private readonly int NumberOfTrials = 10;
private BaseClass _baseClassObject = new BaseClass();
[Benchmark]
public void ObjectTypeAsMyClass()
{
for (var i = 0; i < NumberOfTrials; i++)
{
var x = _baseClassObject as MyClass;
}
}
[Benchmark]
public void ObjectTypeAsMySealedClass()
{
for (var i = 0; i < NumberOfTrials; i++)
{
var x = _baseClassObject as MySealedClass;
}
}
}
Now, running this Benchmark project we would get the following result.
As we can notice here, the performance of casting the object on the sealed class is better than calling it on the non-sealed class.
But why?!!! Let me tell you.
On Non-Sealed Class
While casting the object as MyClass class, the compiler needs to check if the object is of the type MyClass class or any of its child classes.
Thus, this leads to more instructions and for sure more processing and time.
On Sealed Class
While casting the object as MySealedClass class, the compiler needs to check if the object is only of the type MySealedClass class, nothing else. This is because MySealedClass class is sealed and this means that it would never have any child classes.
Thus, this leads to fewer instructions and for sure less processing and time.
Storing Object In Array
To validate if there would be any difference -from the compiler's point of view- between storing an object in an array on both MyClass and MySealedClass classes, we would create a Benchmark project.
[MemoryDiagnoser(false)]
public class Benchmarking
{
private readonly int NumberOfTrials = 10;
private MyClass _myClassObject = new MyClass();
private MySealedClass _mySealedClassObject = new MySealedClass();
private MyClass[] _myClassObjectsArray = new MyClass[1];
private MySealedClass[] _mySealedClassObjectsArray = new MySealedClass[1];
[Benchmark]
public void StoringValuesInMyClassArray()
{
for (var i = 0; i < NumberOfTrials; i++)
{
_myClassObjectsArray[0] = _myClassObject;
}
}
[Benchmark]
public void StoringValuesInMySealedClassArray()
{
for (var i = 0; i < NumberOfTrials; i++)
{
_mySealedClassObjectsArray[0] = _mySealedClassObject;
}
}
}
Now, running this Benchmark project we would get the following result.
As we can notice here, the performance of storing an object in an array on the sealed class is better than calling it on the non-sealed class.
But why?!!! Let me tell you.
Before going into details, let me first remind you of an important point; arrays are covariant.
This means that if we have the following classes defined:
public class A {}
public class B : A {}
Then the following code would be valid:
A[] arrayOfA = new B[5];
Additionally, we can set an item inside arrayOfA to an instance of B as follows:
arrayOfA[0] = new B();
Having said that, let’s now proceed with our main topic.
On Non-Sealed Class
While setting an item inside the myClassObjectsArray array, the compiler needs to check if the instance myClassObject we are using is of type MyClass class or any of its child classes.
Thus, this leads to more instructions and for sure more processing and time.
On Sealed Class
While setting an item inside the mySealedClassObjectsArray array, the compiler needs to check if the instance mySealedClassObject we are using is only of the type MySealedClass class, nothing else. This is because MySealedClass class is sealed and this means that it would never have any child classes.
Thus, this leads to fewer instructions and for sure less processing and time.
Early Failure Detection
In addition to the performance gain we can get by using the sealed keyword, we can also avoid some runtime failures. Let me show you an example.
If we write the following code:
public void Run(MyClass obj)
{
_ = _baseClassObject as IMyInterface;
}
The compiler -at design time- would not show any warnings or errors because actually obj could be of type MyClass class or any of its child classes. Therefore, the compiler needs to wait for the runtime to do the final check.
For sure, if at runtime the real type of obj turns out to not implement IMyInterface, this would cause a runtime exception.
However, if we write the following code:
public void Run(MySealedClass obj)
{
_ = _baseClassObject as IMyInterface;
}
The compiler would show an error (CS0039) in the design time because obj could only be of type MySealedClass class, nothing else. Therefore, the compiler can instantly check if MySealedClass class implements IMyInterface or not.
Thus, this means that using the sealed keyword enabled the compiler to perform the proper static changes at design time.
Final Thoughts
I would always recommend using the sealed keyword whenever applicable.
This is not only for the performance gain you might get but also because it is a best practice from the design point of view to the extent that Microsoft was actually thinking about making all classes sealed by default.
Finally, I hope you enjoyed reading this article as I enjoyed writing it.
Comments