Warum sind versiegelte Typen schneller?

Warum sind versiegelte Typen schneller?

Auf der untersten Ebene kann der Compiler eine Mikrooptimierung vornehmen, wenn Sie Klassen versiegelt haben.

Wenn Sie eine Methode für eine versiegelte Klasse aufrufen und der Typ zur Kompilierzeit als diese versiegelte Klasse deklariert wird, kann der Compiler den Methodenaufruf (in den meisten Fällen) mithilfe der call IL-Anweisung anstelle der callvirt IL-Anweisung implementieren. Dies liegt daran, dass das Methodenziel nicht überschrieben werden kann. Call eliminiert eine Nullprüfung und führt eine schnellere Vtable-Suche durch als callvirt, da es keine virtuellen Tabellen prüfen muss.

Dies kann eine sehr, sehr leichte Leistungsverbesserung sein.

Davon abgesehen würde ich das bei der Entscheidung, ob ich eine Klasse besiegeln soll, völlig ignorieren. Das Markieren eines versiegelten Typs sollte wirklich eine Designentscheidung sein, keine Leistungsentscheidung. Möchten Sie, dass Personen (einschließlich Sie selbst) möglicherweise jetzt oder in Zukunft Unterklassen aus Ihrer Klasse erstellen? Wenn ja, nicht versiegeln. Wenn nicht, versiegeln. Das sollte wirklich der entscheidende Faktor sein.


Im Wesentlichen hat es damit zu tun, dass sie sich nicht um Erweiterungen einer virtuellen Funktionstabelle kümmern müssen; Die versiegelten Typen können nicht erweitert werden, und daher muss sich die Laufzeit nicht darum kümmern, wie sie polymorph sein können.


Es wurde beschlossen, kleine Codebeispiele zu veröffentlichen, um zu veranschaulichen, wann der C#-Compiler „call“- und „callvirt“-Anweisungen ausgibt.

Also, hier ist Quellcode aller Arten, die ich verwendet habe:

    public sealed class SealedClass
    {
        public void DoSmth()
        { }
    }

    public class ClassWithSealedMethod : ClassWithVirtualMethod
    {
        public sealed override void DoSmth()
        { }
    }

    public class ClassWithVirtualMethod
    {
        public virtual void DoSmth()
        { }
    }

Außerdem habe ich eine Methode, die alle "DoSmth()"-Methoden aufruft:

    public void Call()
    {
        SealedClass sc = new SealedClass();
        sc.DoSmth();

        ClassWithVirtualMethod cwcm = new ClassWithVirtualMethod();
        cwcm.DoSmth();

        ClassWithSealedMethod cwsm = new ClassWithSealedMethod();
        cwsm.DoSmth();
    }

Wenn wir uns die "Call()"-Methode ansehen, können wir sagen, dass (theoretisch) der C#-Compiler 2 "callvirt"- und 1 "call"-Anweisungen ausgeben sollte, richtig? Leider sieht die Realität etwas anders aus - 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 
}

Der Grund ist ganz einfach:Die Laufzeit muss prüfen, ob die Typinstanz nicht gleich null ist, bevor die Methode „DoSmth()“ aufgerufen wird.ABER Wir können unseren Code immer noch so schreiben, dass der C#-Compiler optimierten IL-Code ausgeben kann:

    public void Call()
    {
        new SealedClass().DoSmth();

        new ClassWithVirtualMethod().DoSmth();

        new ClassWithSealedMethod().DoSmth();
    }

Ergebnis ist:

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

Wenn Sie versuchen, eine nicht-virtuelle Methode einer nicht versiegelten Klasse auf die gleiche Weise aufzurufen, erhalten Sie auch die Anweisung "call" anstelle von "callvirt"