Подтвердить что ты не робот

Почему запечатанные типы быстрее?

Почему запечатанные типы быстрее?

Мне интересно узнать более подробные сведения о том, почему это правда.

4b9b3361

Ответ 1

На самом низком уровне компилятор может выполнить микро-оптимизацию, когда у вас есть закрытые классы.

Если вы вызываете метод в закрытом классе, и тип объявляется во время компиляции для того, чтобы быть закрытым классом, компилятор может реализовать вызов метода (в большинстве случаев) с помощью команды вызова IL вместо callvirt IL. Это связано с тем, что цель метода не может быть переопределена. Вызов устраняет нулевую проверку и выполняет быстрый поиск vtable, чем callvirt, поскольку он не должен проверять виртуальные таблицы.

Это может быть очень незначительным улучшением производительности.

Говоря, я бы полностью проигнорировал это, когда решал, нужно ли запечатывать класс. Маркировка типа запечатанного действительно должна быть дизайнерским решением, а не решением производительности. Вы хотите, чтобы люди (включая вас) потенциально подкласса из вашего класса, сейчас или в будущем? Если это так, не запечатывайте. Если нет, запечатывайте. Это действительно должно быть решающим фактором.

Ответ 2

По существу, это связано с тем, что им не нужно беспокоиться о расширениях к таблице виртуальных функций; запечатанные типы не могут быть расширены, и поэтому время выполнения не должно беспокоиться о том, как они могут быть полиморфными.

Ответ 3

Решено разместить небольшие образцы кода, чтобы проиллюстрировать, когда компилятор С# испускает инструкции "call" и "callvirt".

Итак, здесь исходный код всех типов, которые я использовал:

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

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

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

Также у меня есть один метод, который вызывает все методы DoSmth():

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

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

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

Глядя на метод "Call()", мы можем сказать, что (теоретически) компилятор С# должен выпустить 2 "callvirt" и 1 "call" инструкции, не так ли? К сожалению, реальность немного отличается - 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 
}

Причина довольно проста: среда выполнения должна проверить, не является ли экземпляр экземпляра равным нулю до вызова метода DoSmth(). НО, мы все еще можем написать наш код таким образом, чтобы компилятор С# мог испускать оптимизированный код IL:

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

        new ClassWithVirtualMethod().DoSmth();

        new ClassWithSealedMethod().DoSmth();
    }

Результат:

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

Если вы попытаетесь вызвать не виртуальный метод непечатаемого класса таким же образом, вы также получите команду "вызов" вместо "callvirt"

Ответ 4

Если компилятор JIT видит вызов виртуального метода с использованием закрытых типов, он может создать более эффективный код, вызвав метод не виртуально. Теперь вызов не виртуального метода происходит быстрее, потому что нет необходимости выполнять поиск vtable. IMHO - это микро-оптимизация, которая должна использоваться в качестве последнего средства для повышения производительности приложения. Если ваш метод содержит любой код, виртуальная версия будет пренебрежимо медленнее, чем не виртуальная, по сравнению с затратами на выполнение самого кода.

Ответ 5

Чтобы продлить ответы других, закрытый класс (эквивалент последнего класса на Java) не может быть расширен. Это означает, что в любое время, когда компилятор видит метод этого класса, компилятор абсолютно знает, что не требуется диспетчеризация во время выполнения. Ему не нужно проверять класс, чтобы динамически видеть, какой метод должен быть вызван классом в иерархии. Это означает, что ветвь может быть скомпилирована, а не динамической.

Например, если у меня есть незапечатанный класс Animal, который имеет метод makeNoise(), компилятор не обязательно знает, отменяет ли этот экземпляр Animal этот метод. Таким образом, каждый раз, когда экземпляр Animal вызывает makeNoise(), необходимо проверить иерархию классов экземпляра, чтобы убедиться, что экземпляр переопределяет этот метод в расширяющемся классе.

Однако, если у меня есть закрытый класс AnimalFeeder, который имеет метод feedAnimal(), то компилятор с уверенностью знает, что этот метод нельзя переопределить. Он может скомпилировать в ветки подпрограмму или эквивалентную инструкцию, а не использовать таблицу виртуальной диспетчеризации.

Примечание. Вы можете использовать sealed для класса, чтобы предотвратить наследование этого класса, и вы можете использовать sealed для метода, объявленного virtual в базовом классе, чтобы предотвратить дальнейшее переопределение этого метода.