Why are sealed types faster?
I am wondering about the deeper details about why this is true.
Why are sealed types faster?
I am wondering about the deeper details about why this is true.
At the lowest level, the compiler can make a micro-optimization when you have sealed classes.
If you're calling a method on a sealed class, and the type is declared at compile time to be that sealed class, the compiler can implement the method call (in most cases) using the call IL instruction instead of the callvirt IL instruction. This is because the method target can not be overridden. Call eliminates a null check and does a faster vtable lookup than callvirt, since it doesn't have to check virtual tables.
This can be a very, very slight improvement to performance.
That being said, I would completely ignore that when deciding whether to seal a class. Marking a type sealed really should be a design decision, not a performance decision. Do you want people (including yourself) to potentially subclass from your class, now or in the future? If so, do not seal. If not, seal. That really should be the deciding factor.
Essentially, it's got to do with the fact that they don't need to have to worry about extensions to a virtual function table; the sealed types can't be extended, and therefore, the runtime doesn't need to be concerned about how they may be polymorphic.
Decided to post small code samples to illustrate when C# compiler emits "call" & "callvirt" instructions.
So, here's source code of all types which I used:
public sealed class SealedClass
{
public void DoSmth()
{ }
}
public class ClassWithSealedMethod : ClassWithVirtualMethod
{
public sealed override void DoSmth()
{ }
}
public class ClassWithVirtualMethod
{
public virtual void DoSmth()
{ }
}
Also I have one method which calls all of "DoSmth()" methods:
public void Call()
{
SealedClass sc = new SealedClass();
sc.DoSmth();
ClassWithVirtualMethod cwcm = new ClassWithVirtualMethod();
cwcm.DoSmth();
ClassWithSealedMethod cwsm = new ClassWithSealedMethod();
cwsm.DoSmth();
}
Looking on "Call()" method we can say that (theoretically) C# compiler should emit 2 "callvirt" & 1 "call" instructions, right? Unfortunately, reality is a bit different - 3 "callvirt"-s:
.method public hidebysig instance void Call() cil managed
{
.maxstack 1
.locals init (
[0] class TestApp.SealedClasses.SealedClass sc,
[1] class TestApp.SealedClasses.ClassWithVirtualMethod cwcm,
[2] class TestApp.SealedClasses.ClassWithSealedMethod cwsm)
L_0000: newobj instance void TestApp.SealedClasses.SealedClass::.ctor()
L_0005: stloc.0
L_0006: ldloc.0
L_0007: callvirt instance void TestApp.SealedClasses.SealedClass::DoSmth()
L_000c: newobj instance void TestApp.SealedClasses.ClassWithVirtualMethod::.ctor()
L_0011: stloc.1
L_0012: ldloc.1
L_0013: callvirt instance void TestApp.SealedClasses.ClassWithVirtualMethod::DoSmth()
L_0018: newobj instance void TestApp.SealedClasses.ClassWithSealedMethod::.ctor()
L_001d: stloc.2
L_001e: ldloc.2
L_001f: callvirt instance void TestApp.SealedClasses.ClassWithVirtualMethod::DoSmth()
L_0024: ret
}
The reason is quite simple: runtime must check if type instance isn't equal to null before calling "DoSmth()" method. BUT we still can write our code in such a way that C# compiler would be able to emit optimized IL code:
public void Call()
{
new SealedClass().DoSmth();
new ClassWithVirtualMethod().DoSmth();
new ClassWithSealedMethod().DoSmth();
}
Result is:
.method public hidebysig instance void Call() cil managed
{
.maxstack 8
L_0000: newobj instance void TestApp.SealedClasses.SealedClass::.ctor()
L_0005: call instance void TestApp.SealedClasses.SealedClass::DoSmth()
L_000a: newobj instance void TestApp.SealedClasses.ClassWithVirtualMethod::.ctor()
L_000f: callvirt instance void TestApp.SealedClasses.ClassWithVirtualMethod::DoSmth()
L_0014: newobj instance void TestApp.SealedClasses.ClassWithSealedMethod::.ctor()
L_0019: callvirt instance void TestApp.SealedClasses.ClassWithVirtualMethod::DoSmth()
L_001e: ret
}
If you try to call non-virtual method of non-sealed class in the same way you will also get "call" instruction instead of "callvirt"
If the JIT compiler sees a call to a virtual method using a sealed types it can produce more efficient code by calling the method non-virtually. Now calling a non-virtual method is faster because there's no need to perform a vtable lookup. IMHO this is micro optimization that should be used as a last resort to improving performance of an application. If your method contains whatever code, the virtual version will be negligibly slower than the non-virtual compared to the cost of executing the code itself.
To extend others' answers, a sealed class (the equivalent of a final class in Java) cannot be extended. This means that any time the compiler sees a method of this class be used, the compiler knows absolutely that no runtime dispatching is needed. It does not have to examine the class to see dynamically which method of which class in the hierarchy needs to be called. This means that the branch can be compiled in rather than being dynamic.
For example, if I have a non-sealed class Animal
that has a method makeNoise()
, the compiler does not necessarily know whether or not any Animal
instance overrides that method. Thus, each time any Animal
instance invokes makeNoise()
, the class hierarchy of the instance needs to be checked to see if the instance overrides this method in an extending class.
However, if I have a sealed class AnimalFeeder
that has a method feedAnimal()
, then the compiler knows with certainty that this method cannot be overridden. It can compile in a branch to subroutine or equivalent instruction rather than using a virtual dispatch table.
Note: You can use sealed
on a class to prevent any inheritance from that class, and you can use sealed
on a method that was declared virtual
in a base class to prevent further overriding of that method.
To really see them you need to analyze the JIT-Compiled code (last one).
C# Code
public sealed class Sealed
{
public string Message { get; set; }
public void DoStuff() { }
}
public class Derived : Base
{
public sealed override void DoStuff() { }
}
public class Base
{
public string Message { get; set; }
public virtual void DoStuff() { }
}
static void Main()
{
Sealed sealedClass = new Sealed();
sealedClass.DoStuff();
Derived derivedClass = new Derived();
derivedClass.DoStuff();
Base BaseClass = new Base();
BaseClass.DoStuff();
}
MIL Code
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 41 (0x29)
.maxstack 8
IL_0000: newobj instance void ConsoleApp1.Program/Sealed::.ctor()
IL_0005: callvirt instance void ConsoleApp1.Program/Sealed::DoStuff()
IL_000a: newobj instance void ConsoleApp1.Program/Derived::.ctor()
IL_000f: callvirt instance void ConsoleApp1.Program/Base::DoStuff()
IL_0014: newobj instance void ConsoleApp1.Program/Base::.ctor()
IL_0019: callvirt instance void ConsoleApp1.Program/Base::DoStuff()
IL_0028: ret
} // end of method Program::Main
JIT- Compiled Code
--- C:\Users\Ivan Porta\source\repos\ConsoleApp1\Program.cs --------------------
{
0066084A in al,dx
0066084B push edi
0066084C push esi
0066084D push ebx
0066084E sub esp,4Ch
00660851 lea edi,[ebp-58h]
00660854 mov ecx,13h
00660859 xor eax,eax
0066085B rep stos dword ptr es:[edi]
0066085D cmp dword ptr ds:[5842F0h],0
00660864 je 0066086B
00660866 call 744CFAD0
0066086B xor edx,edx
0066086D mov dword ptr [ebp-3Ch],edx
00660870 xor edx,edx
00660872 mov dword ptr [ebp-48h],edx
00660875 xor edx,edx
00660877 mov dword ptr [ebp-44h],edx
0066087A xor edx,edx
0066087C mov dword ptr [ebp-40h],edx
0066087F nop
Sealed sealedClass = new Sealed();
00660880 mov ecx,584E1Ch
00660885 call 005730F4
0066088A mov dword ptr [ebp-4Ch],eax
0066088D mov ecx,dword ptr [ebp-4Ch]
00660890 call 00660468
00660895 mov eax,dword ptr [ebp-4Ch]
00660898 mov dword ptr [ebp-3Ch],eax
sealedClass.DoStuff();
0066089B mov ecx,dword ptr [ebp-3Ch]
0066089E cmp dword ptr [ecx],ecx
006608A0 call 00660460
006608A5 nop
Derived derivedClass = new Derived();
006608A6 mov ecx,584F3Ch
006608AB call 005730F4
006608B0 mov dword ptr [ebp-50h],eax
006608B3 mov ecx,dword ptr [ebp-50h]
006608B6 call 006604A8
006608BB mov eax,dword ptr [ebp-50h]
006608BE mov dword ptr [ebp-40h],eax
derivedClass.DoStuff();
006608C1 mov ecx,dword ptr [ebp-40h]
006608C4 mov eax,dword ptr [ecx]
006608C6 mov eax,dword ptr [eax+28h]
006608C9 call dword ptr [eax+10h]
006608CC nop
Base BaseClass = new Base();
006608CD mov ecx,584EC0h
006608D2 call 005730F4
006608D7 mov dword ptr [ebp-54h],eax
006608DA mov ecx,dword ptr [ebp-54h]
006608DD call 00660490
006608E2 mov eax,dword ptr [ebp-54h]
006608E5 mov dword ptr [ebp-44h],eax
BaseClass.DoStuff();
006608E8 mov ecx,dword ptr [ebp-44h]
006608EB mov eax,dword ptr [ecx]
006608ED mov eax,dword ptr [eax+28h]
006608F0 call dword ptr [eax+10h]
006608F3 nop
}
0066091A nop
0066091B lea esp,[ebp-0Ch]
0066091E pop ebx
0066091F pop esi
00660920 pop edi
00660921 pop ebp
00660922 ret
While the creation of the objects is the same, the instruction executed to invoke the methods of the sealed and derived/base class are slightly different. After moving data into registers or RAM (mov instruction), the invoke of the sealed method, execute a comparison between dword ptr [ecx],ecx (cmp instruction) and then call the method while the derived/base class execute directly the method..
According to the report written by Torbj¨orn Granlund, Instruction latencies and throughput for AMD and Intel x86 processors, the speed of the following instruction in a Intel Pentium 4 are:
Link: https://gmplib.org/~tege/x86-timing.pdf
This mean that, ideally, the time needed to invoke a sealed method is 2 cycles while the time needed to invoke a derived or base class method is 3 cycles.
The optimization of the compilers have made the difference between the performances of a sealed and not-sealed classed so low that we are talking about processor circles and for this reason are irrelevant for the majority of applications.