Skip to content

Analyzer Proposal: Seal internal/private types #49944

Closed
dotnet/roslyn-analyzers
#5594
@stephentoub

Description

@stephentoub

There are numerous performance benefits to sealing types:

  1. 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
  1. 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
  1. 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
  1. 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
  1. 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.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-Metacode-analyzerMarks an issue that suggests a Roslyn analyzercode-fixerMarks an issue that suggests a Roslyn code fixerin-prThere is an active PR which will close this issue when it is merged

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions