167

I have come across a lot of optimization tips which say that you should mark your classes as sealed to get extra performance benefits.

I ran some tests to check the performance differential and found none. Am I doing something wrong? Am I missing the case where sealed classes will give better results?

Has anyone run tests and seen a difference?

riQQ
  • 9,878
  • 7
  • 49
  • 66
Vaibhav
  • 11,310
  • 11
  • 51
  • 70
  • 9
    I don't think sealed classes were intended to give a perf increase. The fact that they do may be incidental. In addition to that, profile your app after you've refactored it to use sealed classes, and determine if it was worth the effort. Locking down your extensibility to make an unneeded micro-optimization will cost you in the long run. Of course, if you profiled, and it lets you hit your perf benchmarks (rather than perf for the sake of perf), then you can make a decision as a team if it is worth the money spent. If you have sealed classes for non-perf reasons, then keep em :) – Merlyn Morgan-Graham Sep 03 '10 at 07:02
  • 1
    Have you tried with reflection? I read somewhere that instantiating by reflection is faster with sealed classes – onof Sep 03 '10 at 07:04
  • See this related question on the same topic regarding Java/JVM: http://stackoverflow.com/questions/3961881/why-defining-class-as-final-improves-jvm-performance – andersoj Oct 18 '10 at 18:15
  • To my knowledge there is none. Sealed is there for a different reason - to block extensibility, which may be usefull / needed in a lot of cases. Performance optimization was not a goal here. – TomTom Sep 03 '10 at 07:15
  • ... but if you think about the compiler: if your class is sealed, you know the address of a method you call on your class at compile time. If your class isn't sealed, you have to resolve the method at run-time because you might need to call an override. It's certainly going to be negligible, but I could see that there would be *some* difference. – Dan Puzey Sep 03 '10 at 07:33
  • Yes, but that sort of does not translate into real benefits, as the OP has pointed out. Architectural differences are / may be a lot more relevant. – TomTom Sep 03 '10 at 08:10
  • Seals clas should provide perf-benefit only in presence of virtual/override method. – mathk Jun 28 '12 at 07:49
  • And if the JIT have some inline caching mechanism it is less perf-benefit. – mathk Jun 28 '12 at 07:51
  • For me the only benefit of using sealed class is to make the code simpler. The compiler won't allow to make protected members in such class, so it's one less mistake you can do. Also `IDisposable` implementation is a little easier to do on a sealed class. – Harry Jul 22 '21 at 13:41

12 Answers12

166

The answer was no, sealed classes do not perform better than non-sealed.

2021: The answer is now yes there are performance benefits to sealing a class.

Sealing a class may not always provide a performance boost, but the dotnet team are adopting the rule of sealing all internal classes to give the optimiser the best chance.

For details you can read https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/#peanut-butter

Old answer below.

The issue comes down to the call vs callvirt IL op codes. Call is faster than callvirt, and callvirt is mainly used when you don't know if the object has been subclassed. So people assume that if you seal a class all the op codes will change from calvirts to calls and will be faster.

Unfortunately callvirt does other things that make it useful too, like checking for null references. This means that even if a class is sealed, the reference might still be null and thus a callvirt is needed. You can get around this (without needing to seal the class), but it becomes a bit pointless.

Structs use call because they cannot be subclassed and are never null.

See this question for more information:

Call and callvirt

Cameron MacFarland
  • 70,676
  • 20
  • 104
  • 133
  • 5
    AFAIK, the circumstances where `call` is used are: in the situation `new T().Method()`, for `struct` methods, for non-virtual calls to `virtual` methods (such as `base.Virtual()`), or for `static` methods. Everywhere else uses `callvirt`. – porges Nov 05 '10 at 02:55
  • 1
    Uhh... I realize this is old, but this isn't quite right... the big win with sealed classes is when the JIT Optimizer can inline the call... in that case, the sealed class can be a huge win. – Brian Kennedy Oct 01 '11 at 10:28
  • @BrianKennedy Inlining doesn't provide that much of a performance boost. If it did everyone would be telling you to seal your classes. And most methods cannot be inlined due to complexity. – Cameron MacFarland Oct 02 '11 at 05:17
  • 5
    Why this answer is wrong. From Mono changelog: "Devirtualization optimization for sealed classes and methods, improving IronPython 2.0 pystone performance by 4%. Other programs can expect similar improvement [Rodrigo].". Sealed classes can improve performance, but as always, it depends on situation. – Smilediver Nov 15 '11 at 11:14
  • To anyone who thinks this is wrong - go test it. Any potential speed improvement will be minimal at best. – Cameron MacFarland Nov 15 '11 at 11:17
  • 1
    @Smilediver It can improve performance, but only if you have a bad JIT (no idea how good the .NET JITs are nowadays though - used to be quite bad in that regard). Hotspot for example will inline virtual calls and deoptimize later if necessary - hence you pay the additional overhead only if you actually subclass the class (and even then not necessarily). – Voo Feb 21 '12 at 01:25
  • 1
    -1 The JIT does not necessarily have to generate the same machine code for the same IL opcodes. Null checking and virtual call are orthogonal and separate steps of callvirt. In the case of sealed types the JIT compiler can still optimise part of callvirt. The same goes when the JIT compiler can guarantee that a reference won’t be null. –  Jul 01 '14 at 10:25
  • This answer is outdated now: https://devblogs.microsoft.com/dotnet/performance-improvements-in-ryujit-in-net-core-and-net-framework/ – Sedat Kapanoglu Apr 29 '20 at 22:44
65

The JITter will sometimes use non-virtual calls to methods in sealed classes since there is no way they can be extended further.

There are complex rules regarding calling type, virtual/nonvirtual, and I don't know them all so I can't really outline them for you, but if you google for sealed classes and virtual methods you might find some articles on the topic.

Note that any kind of performance benefit you would obtain from this level of optimization should be regarded as last-resort, always optimize on the algorithmic level before you optimize on the code-level.

Here's one link mentioning this: Rambling on the sealed keyword

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • 2
    the 'rambling' link is interesting in that it sounds like technical goodness but is in fact nonsense. Read the comments on the article for more info. Summary: the 3 reasons given are Versioning, Performance, and Security/Predictability - [see next comment] – Steven A. Lowe Oct 14 '08 at 21:52
  • 1
    [continued] Versioning only applies when there are no subclasses, duh, but extend this argument to every class and suddenly you have no inheritance and guess what, the language is no longer object-oriented (but merely object-based)! [see next] – Steven A. Lowe Oct 14 '08 at 21:53
  • 3
    [continued] the performance example is a joke: optimizing a virtual method call; why would a sealed class have a virtual method in the first place since it cannot be subclassed? Finally, the Security/Predictability argument is clearly fatuous: 'you cannot use it so it's secure/predictable'. LOL! – Steven A. Lowe Oct 14 '08 at 21:55
  • 1
    Did I mention that I loathe sealed classes? There's nothing worse than getting 95% done with an application and discovering that the one feature the customer wants that the library doesn't provide logically belongs in a subclass of a sealed base class. Grrrrr! – Steven A. Lowe Oct 14 '08 at 21:57
  • 16
    @Steven A. Lowe - I think what Jeffrey Richter was trying to say in a slightly roundabout way is that if you leave your class unsealed you need to think about how derived classes can/will use it, and if you don't have the time or inclination to do this properly, then seal it as it's less likely to cause breaking changes in others' code in future. That's not nonsense at all, it's good common sense. – Greg Beech Nov 20 '09 at 09:58
  • 6
    A sealed class might have a virtual method since it might derive from a class that declares it. When you then, later, declare a variable of the sealed descendant class, and call that method, the compiler might emit a direct call to the known implementation, since it knows there is no way that this might be different than what's in the known-at-compile-time vtable for that class. As for sealed/unsealed, that's a different discussion, and I agree with the reasons for making classes sealed by default. – Lasse V. Karlsen Nov 20 '09 at 19:07
  • The provided link to "Rambling on the sealed keyword" is very interesting. Actually the article says that sealing is good and should become the default, but most of the comments say that it is completely wrong and maybe it should be rather totally removed from C# language. I love these contrasts. :-) – Al Kepp Dec 19 '18 at 17:33
34

Update: As of .NET Core 2.0 and .NET Desktop 4.7.1, the CLR now supports devirtualization. It can take methods in sealed classes and replace virtual calls with direct calls - and it can also do this for non-sealed classes if it can figure out it's safe to do so.

In such a case (a sealed class that the CLR couldn't otherwise detect as safe to devirtualise), a sealed class should actually offer some kind of performance benefit.

That said, I wouldn't think it'd be worth worrying about unless you had already profiled the code and determined that you were in a particularly hot path being called millions of times, or something like that:

https://blogs.msdn.microsoft.com/dotnet/2017/06/29/performance-improvements-in-ryujit-in-net-core-and-net-framework/


Original Answer:

I made the following test program, and then decompiled it using Reflector to see what MSIL code was emitted.

public class NormalClass {
    public void WriteIt(string x) {
        Console.WriteLine("NormalClass");
        Console.WriteLine(x);
    }
}

public sealed class SealedClass {
    public void WriteIt(string x) {
        Console.WriteLine("SealedClass");
        Console.WriteLine(x);
    }
}

public static void CallNormal() {
    var n = new NormalClass();
    n.WriteIt("a string");
}

public static void CallSealed() {
    var n = new SealedClass();
    n.WriteIt("a string");
}

In all cases, the C# compiler (Visual studio 2010 in Release build configuration) emits identical MSIL, which is as follows:

L_0000: newobj instance void <NormalClass or SealedClass>::.ctor()
L_0005: stloc.0 
L_0006: ldloc.0 
L_0007: ldstr "a string"
L_000c: callvirt instance void <NormalClass or SealedClass>::WriteIt(string)
L_0011: ret 

The oft-quoted reason that people say sealed provides performance benefits is that the compiler knows the class isn't overriden, and thus can use call instead of callvirt as it doesn't have to check for virtuals, etc. As proven above, this is not true.

My next thought was that even though the MSIL is identical, perhaps the JIT compiler treats sealed classes differently?

I ran a release build under the visual studio debugger and viewed the decompiled x86 output. In both cases, the x86 code was identical, with the exception of class names and function memory addresses (which of course must be different). Here it is

//            var n = new NormalClass();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  cmp         dword ptr ds:[00585314h],0 
0000000d  je          00000014 
0000000f  call        70032C33 
00000014  xor         edx,edx 
00000016  mov         dword ptr [ebp-4],edx 
00000019  mov         ecx,588230h 
0000001e  call        FFEEEBC0 
00000023  mov         dword ptr [ebp-8],eax 
00000026  mov         ecx,dword ptr [ebp-8] 
00000029  call        dword ptr ds:[00588260h] 
0000002f  mov         eax,dword ptr [ebp-8] 
00000032  mov         dword ptr [ebp-4],eax 
//            n.WriteIt("a string");
00000035  mov         edx,dword ptr ds:[033220DCh] 
0000003b  mov         ecx,dword ptr [ebp-4] 
0000003e  cmp         dword ptr [ecx],ecx 
00000040  call        dword ptr ds:[0058827Ch] 
//        }
00000046  nop 
00000047  mov         esp,ebp 
00000049  pop         ebp 
0000004a  ret 

I then thought perhaps running under the debugger causes it to perform less aggressive optimization?

I then ran a standalone release build executable outside of any debugging environments, and used WinDBG + SOS to break in after the program had completed, and view the dissasembly of the JIT compiled x86 code.

As you can see from the code below, when running outside the debugger the JIT compiler is more aggressive, and it has inlined the WriteIt method straight into the caller. The crucial thing however is that it was identical when calling a sealed vs non-sealed class. There is no difference whatsoever between a sealed or nonsealed class.

Here it is when calling a normal class:

Normal JIT generated code
Begin 003c00b0, size 39
003c00b0 55              push    ebp
003c00b1 8bec            mov     ebp,esp
003c00b3 b994391800      mov     ecx,183994h (MT: ScratchConsoleApplicationFX4.NormalClass)
003c00b8 e8631fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c00bd e80e70106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00c2 8bc8            mov     ecx,eax
003c00c4 8b1530203003    mov     edx,dword ptr ds:[3302030h] ("NormalClass")
003c00ca 8b01            mov     eax,dword ptr [ecx]
003c00cc 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00cf ff5010          call    dword ptr [eax+10h]
003c00d2 e8f96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00d7 8bc8            mov     ecx,eax
003c00d9 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c00df 8b01            mov     eax,dword ptr [ecx]
003c00e1 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00e4 ff5010          call    dword ptr [eax+10h]
003c00e7 5d              pop     ebp
003c00e8 c3              ret

Vs a sealed class:

Normal JIT generated code
Begin 003c0100, size 39
003c0100 55              push    ebp
003c0101 8bec            mov     ebp,esp
003c0103 b90c3a1800      mov     ecx,183A0Ch (MT: ScratchConsoleApplicationFX4.SealedClass)
003c0108 e8131fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c010d e8be6f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0112 8bc8            mov     ecx,eax
003c0114 8b1538203003    mov     edx,dword ptr ds:[3302038h] ("SealedClass")
003c011a 8b01            mov     eax,dword ptr [ecx]
003c011c 8b403c          mov     eax,dword ptr [eax+3Ch]
003c011f ff5010          call    dword ptr [eax+10h]
003c0122 e8a96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0127 8bc8            mov     ecx,eax
003c0129 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c012f 8b01            mov     eax,dword ptr [ecx]
003c0131 8b403c          mov     eax,dword ptr [eax+3Ch]
003c0134 ff5010          call    dword ptr [eax+10h]
003c0137 5d              pop     ebp
003c0138 c3              ret

To me, this provides solid proof that there cannot be any performance improvement between calling methods on sealed vs non-sealed classes... I think I'm happy now :-)

Orion Edwards
  • 121,657
  • 64
  • 239
  • 328
  • Any reason why you posted this answer twice instead of closing the other as a duplicate? They look like valid duplicates to me although this isn't my area. – Flexo Feb 20 '12 at 20:52
  • 1
    What if the methods were longer (more than 32 bytes IL code) to avoid inlining and see what call operation would be used? If it's inlined, you don't see the call so can't judge on the effects. – ygoe Jun 28 '15 at 20:07
  • I'm confused: why is there `callvirt` when these methods are not virtual to begin with? – freakish Aug 27 '18 at 21:27
  • 1
    @freakish I can't remember where I saw this, but I read that the CLR used `callvirt` for sealed methods because it still has to null-check the object before invoking the method call, and once you factor that in, you may as well just use `callvirt`. To remove the `callvirt` and just jump directly, they'd either need to modify C# to allow `((string)null).methodCall()` like C++ does, or they'd need to statically prove that the object wasn't null (which they could do, but haven't bothered to) – Orion Edwards Sep 18 '18 at 03:10
  • 2
    Props for going to the effort of digging down to the machine code level, but be _very_ careful making statements like 'this provides solid proof that there _cannot_ be any performance improvement'. What you have shown is that for one particular scenario there is no difference in native output. It's one data point, and one cannot assume it generalises to all scenarios. For starters, your classes don't define any virtual methods, so virtual calls are not required at all. – Matt Craig Dec 15 '19 at 09:44
24

As I know, there is no guarantee of performance benefit. But there is a chance to decrease performance penalty under some specific condition with sealed method. (sealed class makes all methods to be sealed.)

But it's up to compiler implementation and execution environment.


Details

Many of modern CPUs use long pipeline structure to increase performance. Because CPU is incredibly faster than memory, CPU has to prefetch code from memory to accelerate pipeline. If the code is not ready at proper time, the pipelines will be idle.

There is a big obstacle called dynamic dispatch which disrupts this 'prefetching' optimization. You can understand this as just a conditional branching.

// Value of `v` is unknown,
// and can be resolved only at runtime.
// CPU cannot know which code to prefetch.
// Therefore, just prefetch any one of a() or b().
// This is *speculative execution*.
int v = random();
if (v==1) a();
else b();

CPU cannot prefetch next code to execute in this case because the next code position is unknown until the condition is resolved. So this makes hazard causes pipeline idle. And performance penalty by idle is huge in regular.

Similar thing happen in case of method overriding. Compiler may determine proper method overriding for current method call, but sometimes it's impossible. In this case, proper method can be determined only at runtime. This is also a case of dynamic dispatch, and, a main reason of dynamically-typed languages are generally slower than statically-typed languages.

Some CPU (including recent Intel's x86 chips) uses technique called speculative execution to utilize pipeline even on the situation. Just prefetch one of execution path. But hit rate of this technique is not so high. And speculation failure causes pipeline stall which also makes huge performance penalty. (this is completely by CPU implementation. some mobile CPU is known as does not this kind of optimization to save energy)

Basically, C# is a statically compiled language. But not always. I don't know exact condition and this is entirely up to compiler implementation. Some compilers can eliminate possibility of dynamic dispatch by preventing method overriding if the method is marked as sealed. Stupid compilers may not. This is the performance benefit of the sealed.


This answer (Why is it faster to process a sorted array than an unsorted array?) is describing the branch prediction a lot better.

eonil
  • 83,476
  • 81
  • 317
  • 516
  • 1
    Pentium class CPUs prefetch indirect dispatch directly. Sometimes function pointer redirection is faster than if(unguessable) for this reason. – Joshua Feb 03 '11 at 18:52
  • 2
    One advantage of non virtual or sealed function is that they can be inlined in more situations. – CodesInChaos Feb 04 '11 at 10:04
5

<off-topic-rant>

I loathe sealed classes. Even if the performance benefits are astounding (which I doubt), they destroy the object-oriented model by preventing reuse via inheritance. For example, the Thread class is sealed. While I can see that one might want threads to be as efficient as possible, I can also imagine scenarios where being able to subclass Thread would have great benefits. Class authors, if you must seal your classes for "performance" reasons, please provide an interface at the very least so we don't have to wrap-and-replace everywhere that we need a feature you forgot.

Example: SafeThread had to wrap the Thread class because Thread is sealed and there is no IThread interface; SafeThread automatically traps unhandled exceptions on threads, something completely missing from the Thread class. [and no, the unhandled exception events do not pick up unhandled exceptions in secondary threads].

</off-topic-rant>

Steven A. Lowe
  • 60,273
  • 18
  • 132
  • 202
  • 43
    I don't seal my classes for performance reasons. I seal them for design reasons. Designing for inheritance is hard, and that effort will be wasted most of the time. I totally agree about providing interfaces though - that's a *far* superior solution to unsealing classes. – Jon Skeet Oct 31 '08 at 07:09
  • @[Jon Skeet]: do you write classes for libraries that other developers will be using? I don't think that desigining for inheritance is hard, what am I missing? Also, interfaces are great, but they're no substitute for inheritance, they're just better than nothing. – Steven A. Lowe Oct 31 '08 at 17:10
  • 6
    Encapsulation is generally a better solution than inheritance. To take your specific thread example, trapping thread exceptions breaks the Liskov Substitution Principle because you've changed the documented behaviour of the Thread class, so even if you could derive from it, it would not be reasonable to say that you could use SafeThread everywhere you could use Thread. In this case, you would be better to encapsulate Thread into another class which has different documented behaviour, which you are able to do. Sometimes things are sealed for your own good. – Greg Beech Nov 20 '09 at 10:02
  • 2
    @[Greg Beech]: opinion, not fact - being able to inherit from Thread to fix a heinous oversight in its design is NOT a bad thing ;-) And I think you're overstating LSP - the provabable property q(x) in this case is 'an unhandled exception destroys the program' which is not a "desirable property" :-) – Steven A. Lowe Nov 23 '09 at 02:51
  • -1 for not understanding that too much of code flexibility usually only leads to abuse. – Turing Complete Aug 03 '10 at 14:22
  • @Turing interesting set of assumptions; I'm guessing that you drive a slot-car ;-) – Steven A. Lowe Aug 04 '10 at 13:30
  • 1
    No, but I have had my share of crappy code where I enabled other developers to abuse my stuff by not sealing it or by allowing corner cases. Most of my code nowadays is assertions and other contract related stuff. And I'm quite open about the fact that I do this only to be a pain in the ***. – Turing Complete Aug 04 '10 at 13:54
  • Why would `SafeThread` need to be its own class? Why not just have a static factory method somewhere which accepts an `Action` for the thread to be run and an `Action` which gets run after it completes normally or abnormally? Such a method could simply return a `Thread`. – supercat Jan 29 '13 at 23:36
  • @supercat: that would be one useful solution, now. SafeThread was first written in .NET 1.1! – Steven A. Lowe Jan 30 '13 at 05:53
  • @StevenA.Lowe: Is there any reason the same approach couldn't have been used in 1.1 (replacing `Action` with a custom delegate type that takes a parameter of type `Exception`)? – supercat Jan 30 '13 at 13:58
  • @supercat: maybe, didn't think about it - and i wanted exception-thrown and operation-completed events to hook into – Steven A. Lowe Jan 30 '13 at 17:10
  • 2
    Since we're doing off-topic rants here, just like you _loathe_ sealed classes, I _loathe_ swallowed exceptions. There is nothing worse than when something goes titsup but the program carries on. JavaScript is my favourite. You make a change to some code and suddenly clicking a button does absolutely nothing. Great! ASP.NET and UpdatePanel is another one; seriously, if my button handler throws it's a Big Deal and it needs to CRASH, so that I know there's something that needs fixing! A button that does nothing is *more* useless than a button that brings up a crash screen! – Roman Starkov Aug 26 '13 at 23:19
  • While we're doing off-topic rants, someone I know took their Macbook to the Apple Store to get its battery replaced and was told that the battery probably deteriorated due to *overcharging*. I wonder on what basis they made this determination. Maybe they just tell everyone that. But you know, when designing a laptop, *leaving it plugged in* seems like a use case you could design for? Maybe? grrrr – Jason Orendorff Feb 23 '16 at 15:50
  • Hi from 2023. Sealed public classes without an interface are still bad for a very simple reason: any code that uses them is untestable. In the past 15 years the industry has discovered that testing is good. Good luck trying to test anything involving a `HttpListener` or `HttpListenerContext` in .net. – ATOMP Mar 03 '23 at 22:15
4

Marking a class sealed should have no performance impact.

There are cases where csc might have to emit a callvirt opcode instead of a call opcode. However, it seems those cases are rare.

And it seems to me that the JIT should be able to emit the same non-virtual function call for callvirt that it would for call, if it knows that the class doesn't have any subclasses (yet). If only one implementation of the method exists, there's no point loading its address from a vtable—just call the one implementation directly. For that matter, the JIT can even inline the function.

It's a bit of a gamble on the JIT's part, because if a subclass is later loaded, the JIT will have to throw away that machine code and compile the code again, emitting a real virtual call. My guess is this doesn't happen often in practice.

(And yes, VM designers really do aggressively pursue these tiny performance wins.)

Jason Orendorff
  • 42,793
  • 6
  • 62
  • 96
3

Sealed classes should provide a performance improvement. Since a sealed class cannot be derived, any virtual members can be turned into non-virtual members.

Of course, we're talking really small gains. I wouldn't mark a class as sealed just to get a performance improvement unless profiling revealed it to be a problem.

Karl Seguin
  • 21,574
  • 5
  • 44
  • 49
  • 1
    They *should*, but it appears that they do not. If the CLR had the concept of non-null reference types, then a sealed class really would be better, as the compiler could emit `call` instead of `callvirt`... I'd love non-null reference types for many many other reasons too... sigh :-( – Orion Edwards Feb 20 '12 at 21:04
3

I consider "sealed" classes the normal case and I ALWAYS have a reason to omit the "sealed" keyword.

The most important reasons for me are:

a) Better compile time checks (casting to interfaces not implemented will be detected at compile time, not only at runtime)

and, top reason:

b) Abuse of my classes is not possible that way

I wish Microsoft would have made "sealed" the standard, not "unsealed".

Turing Complete
  • 929
  • 2
  • 12
  • 19
  • I believe "omit" (to leave out) should be "emit" (to produce)? – user2864740 Feb 14 '17 at 06:35
  • @user2864740: No, I believe Turing is saying they have to have a very good reason to use a non-sealed class. – Julia Apr 17 '22 at 23:29
  • 1
    @Julia Indeed. On a 5-year later re-read, that’s how I read the sentence. I will *emit* the sealed keyword unless there are reasons otherwise ;-) – user2864740 Jun 05 '22 at 19:09
3

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
3

Starting from .NET 6.0 the answer is yes.

Sealing a class can help the JIT de-virtualize calls, resulting in less overhead when calling a method. This has additional benefits, because the de-virtualized call can be inlined by the JIT if necessary, which can also lead to constant folding.

For example, in this code from the MSDN article:

[Benchmark(Baseline = true)]
public int NonSealed() => _nonSealed.M() + 42;

[Benchmark]
public int Sealed() => _sealed.M() + 42;

public class BaseType
{
    public virtual int M() => 1;
}

public class NonSealedType : BaseType
{
    public override int M() => 2;
}

public sealed class SealedType : BaseType
{
    public override int M() => 2;
}

The "NonSealed" benchmark runs in 0.9837ns, but the "Sealed" method doesn't take more time than a function that simply returns a constant value. This is due to constant folding.

Type checking sealed classes also has performance benefits, like in this code from the MSDN article:

private object _o = "hello";

[Benchmark(Baseline = true)]
public bool NonSealed() => _o is NonSealedType;

[Benchmark]
public bool Sealed() => _o is SealedType;

public class NonSealedType { }
public sealed class SealedType { }

Checking against a non-sealed type takes ~1.76ns, while checking the sealed type is only ~0.07ns.

In fact, the .NET team made a policy to seal all the private and internal classes that can be sealed.

Notice that we're dealing with saving less than 2 nanoseconds on a call, so the overhead of calling a virtual method is not gonna be the bottleneck most of the time. I think it's more appropriate for simple virtual getters or very short methods.

asaf92
  • 1,557
  • 1
  • 19
  • 30
  • _In fact, the .NET team made a policy to seal all the private and internal classes that can be sealed._ Can you please provide sources of this information? – SysGen Feb 21 '22 at 09:10
  • 1
    From the link provided: _"...we’ve adopted a general policy that all non-public types that can be sealed should be, so as to maximize the chances use of these types will simply be better than it otherwise would be."_ – asaf92 Feb 21 '22 at 10:13
2

sealed classes will be at least a tiny bit faster, but sometimes can be waayyy faster... if the JIT Optimizer can inline calls that would have otherwise been virtual calls. So, where there's oft-called methods that are small enough to be inlined, definitely consider sealing the class.

However, the best reason to seal a class is to say "I didn't design this to be inherited from, so I'm not going to let you get burned by assuming it was designed to be so, and I'm not going to burn myself by getting locked into an implementation because I let you derive from it."

I know some here have said they hate sealed classes because they want the opportunity to derive from anything... but that is OFTEN not the most maintainable choice... because exposing a class to derivation locks you in a lot more than not exposing all that. Its similar to saying "I loathe classes that have private members... I often can't make the class do what I want because I don't have access." Encapsulation is important... sealing is one form of encapsulation.

Brian Kennedy
  • 3,499
  • 3
  • 21
  • 27
  • The C# compiler will still use `callvirt` (virtual method call) for instance methods on sealed classes, because it still has to do a null-object check on them. Regarding inlining, the CLR JIT can (and does) inline virtual method calls for both sealed and non-sealed classes... so yeah. The performance thing is a myth. – Orion Edwards Feb 20 '12 at 21:02
-11

Run this code and you'll see that sealed classes are 2 times faster:

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new SealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("Sealed class : {0}", watch.Elapsed.ToString());

        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new NonSealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("NonSealed class : {0}", watch.Elapsed.ToString());

        Console.ReadKey();
    }
}

sealed class SealedClass
{
    public string GetName()
    {
        return "SealedClass";
    }
}

class NonSealedClass
{
    public string GetName()
    {
        return "NonSealedClass";
    }
}

output: Sealed class : 00:00:00.1897568 NonSealed class : 00:00:00.3826678

RuslanG
  • 13
  • 1
  • 10
    There's a couple of problems with this. First off, you're not resetting the stopwatch between the first and second test. Second, the way you're calling the method means that all op codes will be call not callvirt so the type doesn't matter. – Cameron MacFarland Dec 17 '09 at 22:49
  • 1
    RuslanG, You forgot to call watch.Reset() after running first test. o_O :] –  Jan 15 '11 at 00:00
  • Yes, the code above has bugs. Then again, who can brag about oneself to never introduce mistakes in code? **But** this answer has one important thing better than all others: It tries to measure the effect in question. And this answer also shares the code for all of you inspect and [mass-]downvote (I wonder why is the mass-downvoting necessary?). In contrast to other answers. That deserves respect in my opinion... Also please note that the user is newbie, so not very welcoming approach either. Some simple fixes and this code becomes useful for all of us. Fix + share fixed version, if you dare – Roland Pihlakas Dec 08 '16 at 00:07
  • I disagree with @RolandPihlakas. As you said, "who can brag about oneself to never introduce mistakes in code". Answer -> No, none. But that's not the point. The point is, it is a wrong information which can mislead another new programmer. Many people can easily miss that the stopwatch was not reset. They could believe that the benchmark information is true. Is not that more harmful? Someone who is quickly looking for an answer may not even read these comments, rather he/she would see an answer and he/she may believe that. – Emran Hussain May 17 '17 at 02:37