Действительно ли закрытые классы обеспечивают преимущества в производительности?

StackOverflow https://stackoverflow.com/questions/2134

Вопрос

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

Я провел несколько тестов, чтобы проверить разницу в производительности, и не нашел ни одного.Я делаю что-то не так?Я упускаю тот случай, когда закрытые классы дадут лучшие результаты?

Кто-нибудь проводил тесты и видел разницу?

Помоги мне учиться :)

Это было полезно?

Решение

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

Существуют сложные правила, касающиеся типа вызова, виртуального / невиртуального, и я не знаю их всех, поэтому не могу подробно описать их для вас, но если вы загуглите закрытые классы и виртуальные методы, вы можете найти несколько статей по этой теме.

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

Вот одна ссылка, упоминающая об этом: Бессвязное использование ключевого слова sealed

Другие советы

Ответ отрицательный, запечатанные классы работают не лучше, чем не запечатанные.

Проблема сводится к тому, что call против callvirt IL op codes. Call быстрее, чем callvirt, и callvirt в основном используется, когда вы не знаете, был ли объект отнесен к подклассу.Таким образом, люди предполагают, что если вы запечатаете класс, все операционные коды изменятся с calvirts Для calls и будет быстрее.

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

Использование структур call потому что они не могут быть подклассами и никогда не являются нулевыми.

Смотрите этот вопрос для получения дополнительной информации:

Звоните и callvirt

Обновить:Начиная с .NET Core 2.0 и .NET Desktop 4.7.1, среда CLR теперь поддерживает девиртуализацию.Он может принимать методы в закрытых классах и заменять виртуальные вызовы прямыми вызовами - и он также может делать это для незапечатанных классов, если он может решить, что это безопасно.

В таком случае (закрытый класс, который среда CLR иначе не смогла бы определить как безопасный для девиртуализации), закрытый класс действительно должен предлагать какое-то преимущество в производительности.

Тем не менее, я бы не подумал, что об этом стоит беспокоиться если не вы уже профилировали код и определили, что находитесь на особенно горячем пути, который вызывается миллионы раз, или что-то в этом роде:

https://blogs.msdn.microsoft.com/dotnet/2017/06/29/performance-improvements-in-ryujit-in-net-core-and-net-framework/


Оригинальный Ответ:

Я создал следующую тестовую программу, а затем декомпилировал ее с помощью Reflector, чтобы увидеть, какой код MSIL был выдан.

public class NormalClass {
    public void WriteIt(string x) {
        Console.WriteLine("NormalClass");
        Console.WriteLine(x);
    }
}

public sealed class SealedClass {
    public void WriteIt(string x) {
        Console.WriteLine("SealedClass");
        Console.WriteLine(x);
    }
}

public static void CallNormal() {
    var n = new NormalClass();
    n.WriteIt("a string");
}

public static void CallSealed() {
    var n = new SealedClass();
    n.WriteIt("a string");
}

Во всех случаях компилятор C # (Visual studio 2010 в конфигурации Release build) выдает идентичный MSIL, который выглядит следующим образом:

L_0000: newobj instance void <NormalClass or SealedClass>::.ctor()
L_0005: stloc.0 
L_0006: ldloc.0 
L_0007: ldstr "a string"
L_000c: callvirt instance void <NormalClass or SealedClass>::WriteIt(string)
L_0011: ret 

Часто цитируемая причина, по которой люди говорят, что sealed обеспечивает преимущества в производительности, заключается в том, что компилятор знает, что класс не переопределен, и, следовательно, может использовать call вместо того, чтобы callvirt поскольку ему не нужно проверять наличие виртуалов и т.д.Как было доказано выше, это неправда.

Моей следующей мыслью было, что, хотя MSIL идентичен, возможно, JIT-компилятор по-разному обрабатывает закрытые классы?

Я запустил сборку релиза в отладчике Visual Studio и просмотрел декомпилированный вывод x86.В обоих случаях код x86 был идентичным, за исключением имен классов и адресов памяти функций (которые, конечно, должны отличаться).Вот оно

//            var n = new NormalClass();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  cmp         dword ptr ds:[00585314h],0 
0000000d  je          00000014 
0000000f  call        70032C33 
00000014  xor         edx,edx 
00000016  mov         dword ptr [ebp-4],edx 
00000019  mov         ecx,588230h 
0000001e  call        FFEEEBC0 
00000023  mov         dword ptr [ebp-8],eax 
00000026  mov         ecx,dword ptr [ebp-8] 
00000029  call        dword ptr ds:[00588260h] 
0000002f  mov         eax,dword ptr [ebp-8] 
00000032  mov         dword ptr [ebp-4],eax 
//            n.WriteIt("a string");
00000035  mov         edx,dword ptr ds:[033220DCh] 
0000003b  mov         ecx,dword ptr [ebp-4] 
0000003e  cmp         dword ptr [ecx],ecx 
00000040  call        dword ptr ds:[0058827Ch] 
//        }
00000046  nop 
00000047  mov         esp,ebp 
00000049  pop         ebp 
0000004a  ret 

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

Затем я запустил исполняемый файл автономной сборки выпуска вне каких-либо сред отладки и использовал WinDbg + SOS для взлома после завершения программы и просмотра разборки JIT-скомпилированного кода x86.

Как вы можете видеть из приведенного ниже кода, при запуске вне отладчика JIT-компилятор работает более агрессивно, и в него встроен WriteIt метод прямо в вызывающий объект.Однако важно то, что он был идентичен при вызове запечатанного и не запечатанного класса.Нет никакой разницы между запечатанным и негерметичным классом.

Вот это происходит при вызове обычного класса:

Normal JIT generated code
Begin 003c00b0, size 39
003c00b0 55              push    ebp
003c00b1 8bec            mov     ebp,esp
003c00b3 b994391800      mov     ecx,183994h (MT: ScratchConsoleApplicationFX4.NormalClass)
003c00b8 e8631fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c00bd e80e70106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00c2 8bc8            mov     ecx,eax
003c00c4 8b1530203003    mov     edx,dword ptr ds:[3302030h] ("NormalClass")
003c00ca 8b01            mov     eax,dword ptr [ecx]
003c00cc 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00cf ff5010          call    dword ptr [eax+10h]
003c00d2 e8f96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00d7 8bc8            mov     ecx,eax
003c00d9 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c00df 8b01            mov     eax,dword ptr [ecx]
003c00e1 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00e4 ff5010          call    dword ptr [eax+10h]
003c00e7 5d              pop     ebp
003c00e8 c3              ret

Против закрытого класса:

Normal JIT generated code
Begin 003c0100, size 39
003c0100 55              push    ebp
003c0101 8bec            mov     ebp,esp
003c0103 b90c3a1800      mov     ecx,183A0Ch (MT: ScratchConsoleApplicationFX4.SealedClass)
003c0108 e8131fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c010d e8be6f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0112 8bc8            mov     ecx,eax
003c0114 8b1538203003    mov     edx,dword ptr ds:[3302038h] ("SealedClass")
003c011a 8b01            mov     eax,dword ptr [ecx]
003c011c 8b403c          mov     eax,dword ptr [eax+3Ch]
003c011f ff5010          call    dword ptr [eax+10h]
003c0122 e8a96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0127 8bc8            mov     ecx,eax
003c0129 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c012f 8b01            mov     eax,dword ptr [ecx]
003c0131 8b403c          mov     eax,dword ptr [eax+3Ch]
003c0134 ff5010          call    dword ptr [eax+10h]
003c0137 5d              pop     ebp
003c0138 c3              ret

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

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

Но это зависит от реализации компилятора и среды выполнения.


Подробные сведения

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

Существует большое препятствие, называемое динамическая отправка что нарушает эту оптимизацию "предварительной выборки".Вы можете понимать это просто как условное ветвление.

// Value of `v` is unknown,
// and can be resolved only at runtime.
// CPU cannot know which code to prefetch.
// Therefore, just prefetch any one of a() or b().
// This is *speculative execution*.
int v = random();
if (v==1) a();
else b();

В этом случае процессор не может выполнить предварительную выборку следующего кода для выполнения, поскольку следующая позиция кода неизвестна до тех пор, пока не будет выполнено условие.Таким образом, это делает опасность приводит к простаиванию конвейера.А снижение производительности при простаивании в обычном режиме огромно.

Аналогичная вещь происходит и в случае переопределения метода.Компилятор может определить правильное переопределение метода для текущего вызова метода, но иногда это невозможно.В этом случае правильный метод может быть определен только во время выполнения.Это также случай динамической отправки, и это основная причина, по которой динамически типизированные языки обычно работают медленнее, чем статически типизированные языки.

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

По сути, C # - это статически скомпилированный язык.Но не всегда.Я не знаю точного условия, и это полностью зависит от реализации компилятора.Некоторые компиляторы могут исключить возможность динамической отправки, предотвращая переопределение метода, если метод помечен как sealed.Глупые компиляторы не могут этого сделать.В этом заключается преимущество производительности sealed.


Этот ответ (Почему сортированный массив обрабатывается быстрее, чем несортированный?) намного лучше описывает предсказание ветвления.

Маркировка класса sealed это не должно влиять на производительность.

Бывают случаи , когда csc возможно, придется испускать callvirt код операции вместо call код операции.Однако, похоже, такие случаи редки.

И мне кажется, что JIT должен иметь возможность выдавать один и тот же вызов невиртуальной функции для callvirt что это было бы для call, если он знает, что у класса нет никаких подклассов (пока).Если существует только одна реализация метода, нет смысла загружать его адрес из виртуальной таблицы — просто вызовите одну реализацию напрямую.Если уж на то пошло, JIT может даже встроить функцию.

Это небольшая авантюра со стороны JIT, потому что если подкласс является позже загруженный JIT должен будет выбросить этот машинный код и снова скомпилировать код, выполнив реальный виртуальный вызов.Я предполагаю, что на практике это случается нечасто.

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

Закрытые классы следует обеспечьте повышение производительности.Поскольку закрытый класс не может быть производным, любые виртуальные члены могут быть преобразованы в невиртуальные члены.

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

<off-topic-rant>

Я ненавидеть закрытые классы.Даже если преимущества в производительности поразительны (в чем я сомневаюсь), они уничтожить объектно-ориентированная модель, предотвращающая повторное использование через наследование.Например, класс Thread является закрытым.Хотя я вижу, что можно захотеть, чтобы потоки были как можно более эффективными, я также могу представить сценарии, в которых возможность создания подкласса Thread имела бы большие преимущества.Авторы класса, если вы должен закрывайте свои занятия по соображениям "производительности", пожалуйста, предоставьте интерфейс по крайней мере, чтобы нам не приходилось переносить и заменять везде, где нам нужна функция, которую вы забыли.

Пример: Безопасная резьба пришлось обернуть класс Thread, потому что Thread запечатан и интерфейса IThread нет;SafeThread автоматически улавливает необработанные исключения в потоках, чего полностью нет в классе Thread.[и нет, необработанные события исключения делают нет извлекать необработанные исключения во вторичных потоках].

</off-topic-rant>

Я считаю "запечатанные" классы обычным случаем, и у меня ВСЕГДА есть причина опустить ключевое слово "запечатанный".

Самыми важными причинами для меня являются:

a) Улучшенные проверки во время компиляции (приведение к не реализованным интерфейсам будет обнаружено во время компиляции, а не только во время выполнения)

и, главная причина:

б) Злоупотребление моими занятиями таким образом невозможно

Я бы хотел, чтобы Microsoft сделала "запечатанный" стандартом, а не "незапечатанный".

@Vaibhav, какие тесты вы проводили для измерения производительности?

Я думаю, что нужно было бы использовать Ротор а также углубиться в CLI и понять, как закрытый класс повысит производительность.

SSCLI (Ротор)
SSCLI:Инфраструктура общего языка с общим исходным кодом

Инфраструктура общего языка (CLI) - это стандарт ECMA, который описывает ядро .NET Framework.CLI с общим исходным кодом (SSCLI), также известный как Rotor, представляет собой сжатый архив исходного кода для рабочей реализации CLI ECMA и языка ECMA C # спецификация, технологии, лежащие в основе архитектуры Microsoft .NET .

закрытые классы будут, по крайней мере, немного быстрее, но иногда могут быть намного быстрее...если JIT-оптимизатор может выполнять встроенные вызовы, которые в противном случае были бы виртуальными вызовами.Итак, там, где есть часто вызываемые методы, которые достаточно малы, чтобы их можно было встроить, определенно рассмотрите возможность герметизации класса.

Однако лучшая причина запечатать класс - сказать: "Я разрабатывал это не для того, чтобы наследовать от него, поэтому я не собираюсь позволять вам обжечься, предполагая, что так оно и было задумано, и я не собираюсь обжигаться, привязываясь к реализации, потому что я позволяю вам наследовать от нее".

Я знаю, что некоторые здесь говорили, что ненавидят закрытые классы, потому что хотят иметь возможность извлекать выгоду из чего угодно...но часто это не самый удобный выбор...потому что предоставление класса для деривации блокирует вас гораздо больше, чем отсутствие всего этого.Это похоже на высказывание "Я ненавижу классы, в которых есть частные участники...Я часто не могу заставить класс делать то, что я хочу, потому что у меня нет доступа ". Важна инкапсуляция...герметизация - это одна из форм инкапсуляции.

Запустите этот код, и вы увидите, что закрытые классы работают в 2 раза быстрее:

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new SealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("Sealed class : {0}", watch.Elapsed.ToString());

        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new NonSealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("NonSealed class : {0}", watch.Elapsed.ToString());

        Console.ReadKey();
    }
}

sealed class SealedClass
{
    public string GetName()
    {
        return "SealedClass";
    }
}

class NonSealedClass
{
    public string GetName()
    {
        return "NonSealedClass";
    }
}

выходной сигнал:Запечатанный класс :00:00:00.1897568 Незапечатанный класс :00:00:00.3826678

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top