38

Why are sealed types faster?

I am wondering about the deeper details about why this is true.

Joan Venge
  • 315,713
  • 212
  • 479
  • 689
  • 2
    Please see: http://stackoverflow.com/questions/268251/why-seal-a-class – Shog9 May 26 '09 at 16:58
  • Are they? I don't know... the CLR may be able to optimize the method dispatch table, knowing that it can't grow anymore. – harpo May 26 '09 at 16:59
  • 1
    @harpo: see this reference: http://msdn.microsoft.com/en-us/library/ms173150.aspx. I had it added in to my answer, but the simple fact is that they don't say much about WHY, so I decided not to add it... – Paul Sonier May 26 '09 at 17:00
  • http://stackoverflow.com/questions/268251/ –  Mar 27 '13 at 15:56

6 Answers6

44

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.

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • 6
    When designing, it may be a good idea to lean towards sealing public types that don't explicitly need to be extended since unsealing a class in a future version is a non-breaking change while the reverse is not true. – Neil Williams May 26 '09 at 17:28
  • 2
    @Neil Williams: I agree. In general, since unsealing a class is safe, and sealing is not, if you're making public libraries, sealing can be a nice thing to do. Again, though, this makes sealing a design choice more than a performance issue. – Reed Copsey May 26 '09 at 17:34
  • I thought that it was due to inlining. The C# compiler always uses callvirt because it likes the null-check side effect of that IL code. – Two Bit Gangster Sep 08 '09 at 17:01
10

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.

Paul Sonier
  • 38,903
  • 3
  • 77
  • 117
8

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"

  • Thanks, why the null check is avoided in your second example? Can you please explain? – Joan Venge Jun 03 '09 at 07:45
  • 1
    Because I don't use local variable of "SealedClass" type as in first example, so compiler doesn't need to check if it's 'null'. Same IL code will be generated if you declare "SealedClass.DoSmth()" method as static – Volodymyr Usarskyy Jun 03 '09 at 14:26
5

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.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 2
    Why should this be a last resort? Why not just seal your classes by default? It's usually only considered a microopimization if there's some cost associated with it (less readable code, or more development time typically). If there's no downside to doing it, why not do it whether or not performance is an issue? – jalf May 26 '09 at 17:24
  • 1
    When you seal a class, you prevent use of inheritance. This can make development more difficult, it can prevent working around certain bugs. Ideally, one would think about it and design for inheritance, and make plain what is designed to be extended and seal everything else. Blindly sealing everything is too restrictive. – Eddie May 26 '09 at 19:06
4

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.

Eddie
  • 53,828
  • 22
  • 125
  • 145
1

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:

  • mov: has 1 cycle as latency and the processor can sustain 2.5 instructions per cycle of this type
  • cmp: has 1 cycle as latency and the processor can sustain 2 instructions per cycle of this type

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.

GTRekter
  • 905
  • 11
  • 21