The first thing to note is that C# generally makes virtual calls to any instance method on a reference type, even when the method isn't virtual. This is because there is a C# rule that it's illegal to call a method on a null reference that isn't a .NET rule (in raw CIL if you call a method on a null reference and that method doesn't itself access a field or virtual method, it works fine) and using callvirt
is a cheap way to enforce that rule.
C# will generate call
rather than callvirt
for non-virtual instance calls in some cases where it is obvious that the reference isn't null. In particular with obj?.SomeMethod()
since the ?.
means a null-check is already happening, then if SomeMethod()
isn't virtual this will be compiled to call
. This only happens with ?.
since the code for compiling ?.
can do that check whereas it doesn't happen with if (obj != null){obj.SomeMethod();}
because the code compiling .
doesn't know it is following a null-check. The logic involved is very localised.
It is possible at the CIL level to skip a virtual table lookup for virtual methods. This is how base
calls work; compiled to a call
on the base's implementation of a method rather than a callvirt
. By extension in the construct obj?.SomeMethod()
where SomeMethod
was virtual and sealed (whether individually or because the type of obj
was sealed) then it would be theoretically possible to compile that as a call
to the most derived type's implementation. There are though some extra checks that need to be made in particular to make sure it still works correctly if classes in the hierarchy between the declaring type and the sealed type add or remove overrides. It would require some global knowledge of the hierarchy (and assurance that knowledge won't change, which means all the types being in the assembly currently being compiled) for the optimisation to be safe. And the gain is tiny. And still not available most of the time for the same reason that callvirt
is used even on non-virtual calls most of the time.
I don't think there's anywhere where sealed
affects how the compiler generates the call, and it certainly isn't affecting it most of the time.
The jitter is free to apply more knowledge though, but again the difference if any is going to very small. I'd certainly recommend marking classes you know won't be overridden as sealed
and if the jitter makes use of that then that's great, but the main reason I'd recommend it isn't performance but rather correctness. If you somewhere try to override a class that you had marked sealed
then either A. You've just changed the design a bit and knew you'd have to remove the sealed
(.5seconds work to remove it) or B. You've done something in one place you were sure you wouldn't in another. It's good to have the brief pause of reconsidering.
If I make a class sealed does this mean that all virtual methods in class hierarchy are also marked sealed?
They are considered sealed the same as if explicitly marked as such.