Почему запечатанные типы быстрее? - PullRequest
34 голосов
/ 26 мая 2009

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

Меня интересуют более глубокие детали того, почему это так.

Ответы [ 5 ]

36 голосов
/ 26 мая 2009

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

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

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

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

10 голосов
/ 26 мая 2009

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

8 голосов
/ 02 июня 2009

Решено опубликовать небольшие примеры кода, чтобы проиллюстрировать, когда компилятор C # отправляет инструкции "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 ()", можно сказать, что (теоретически) компилятор C # должен выдавать 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 (). НО мы все еще можем написать наш код таким образом, чтобы компилятор C # мог генерировать оптимизированный код 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 
}

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

5 голосов
/ 26 мая 2009

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

3 голосов
/ 26 мая 2009

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...