Description
There are numerous performance benefits to sealing types:
- Calls to overrides can be done directly rather than with virtual dispatch, which then also means they can be inlined.
public class C {
internal void Call(SealedType o) => o.M();
internal void Call(NonSealedType o) => o.M();
}
internal class BaseType
{
public virtual void M() {}
}
internal class NonSealedType : BaseType
{
public override void M() {}
}
internal sealed class SealedType : BaseType
{
public override void M() {}
}
results in:
C.Call(SealedType)
L0000: cmp [rdx], edx
L0002: ret
C.Call(NonSealedType)
L0000: mov rcx, rdx
L0003: mov rax, [rdx]
L0006: mov rax, [rax+0x40]
L000a: mov rax, [rax+0x20]
L000e: jmp rax
is
/as
type checks for the type can be done more efficiently, as it only needs to compare the type itself rather than account for a potential hierarchy.
public class C {
public bool IsSealed(Object o) => o is SealedType;
public bool IsNotSealed(Object o) => o is NonSealedType;
}
internal class NonSealedType { }
internal sealed class SealedType { }
results in:
C.IsSealed(System.Object)
L0000: test rdx, rdx
L0003: je short L0016
L0005: mov rax, 0x7ff7ab9ad180
L000f: cmp [rdx], rax
L0012: je short L0016
L0014: xor edx, edx
L0016: test rdx, rdx
L0019: setne al
L001c: movzx eax, al
L001f: ret
C.IsNotSealed(System.Object)
L0000: sub rsp, 0x28
L0004: mov rcx, 0x7ff7ab9ad048
L000e: call System.Runtime.CompilerServices.CastHelpers.IsInstanceOfClass(Void*, System.Object)
L0013: test rax, rax
L0016: setne al
L0019: movzx eax, al
L001c: add rsp, 0x28
L0020: ret
- Arrays of that type don’t need covariance checks every time an element is stored into it.
public class C {
internal void StoreSealed(SealedType[] arr, SealedType item) => arr[0] = item;
internal void StoreNonSealed(NonSealedType[] arr, NonSealedType item) => arr[0] = item;
}
internal class NonSealedType { }
internal sealed class SealedType { }
results in:
C.StoreSealed(SealedType[], SealedType)
L0000: sub rsp, 0x28
L0004: cmp dword ptr [rdx+8], 0
L0008: jbe short L001c
L000a: lea rcx, [rdx+0x10]
L000e: mov rdx, r8
L0011: call 0x00007ff801db9f80
L0016: nop
L0017: add rsp, 0x28
L001b: ret
L001c: call 0x00007ff801f0bc70
L0021: int3
C.StoreNonSealed(NonSealedType[], NonSealedType)
L0000: sub rsp, 0x28
L0004: mov rcx, rdx
L0007: xor edx, edx
L0009: call System.Runtime.CompilerServices.CastHelpers.StelemRef(System.Array, Int32, System.Object)
L000e: nop
L000f: add rsp, 0x28
L0013: ret
- Creating spans of that type don’t need to validate the actual type of the array matches the specified generic type.
using System;
public class C {
internal Span<SealedType> CreateSealed(SealedType[] arr) => arr;
internal Span<NonSealedType> CreateNonSealedType(NonSealedType[] arr) => arr;
}
internal class NonSealedType { }
internal sealed class SealedType { }
results in:
C.CreateSealed(SealedType[])
L0000: test r8, r8
L0003: jne short L000b
L0005: xor eax, eax
L0007: xor ecx, ecx
L0009: jmp short L0013
L000b: lea rax, [r8+0x10]
L000f: mov ecx, [r8+8]
L0013: mov [rdx], rax
L0016: mov [rdx+8], ecx
L0019: mov rax, rdx
L001c: ret
C.CreateNonSealedType(NonSealedType[])
L0000: sub rsp, 0x28
L0004: test r8, r8
L0007: jne short L000f
L0009: xor eax, eax
L000b: xor ecx, ecx
L000d: jmp short L0026
L000f: mov rax, 0x7ff7aba4d870
L0019: cmp [r8], rax
L001c: jne short L0034
L001e: lea rax, [r8+0x10]
L0022: mov ecx, [r8+8]
L0026: mov [rdx], rax
L0029: mov [rdx+8], ecx
L002c: mov rax, rdx
L002f: add rsp, 0x28
L0033: ret
L0034: call System.ThrowHelper.ThrowArrayTypeMismatchException()
L0039: int3
- Probably more both I’ve missed and that will emerge in the future.
We should add an analyzer, at either hidden or info level but that we’d look to turn on as a warning in dotnet/runtime, that flags:
- Internal and private non-static, non-abstract, non-sealed classes that
- Don’t have any derived types in its containing assembly
- Which also doesn't have InternalsVisibleTo on it
and flag that they should be sealed. A fixer would seal the type. We could also make it configurable on the visibility, in case someone wanted to e.g. also opt-in public types (we wouldn't/couldn't in dotnet/runtime). We could also optionally factor in whether the type declares any new virtual methods, a protected ctor, or anything else that suggests the type is intended for derivation... upon detecting such things, we could either choose not to warn, or we could warn with a different diagnostic id.
If a developer working in the library ever wants to derive from such a type, they can remove the sealed when they add the derivation.