Действительно ли закрытые классы предлагают преимущества в производительности? - PullRequest
127 голосов
/ 05 августа 2008

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

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

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

Помоги мне выучить:)

Ответы [ 11 ]

138 голосов
/ 24 декабря 2008

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

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

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

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

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

Звоните и звоните

53 голосов
/ 05 августа 2008

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

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

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

Вот одна ссылка, на которой упоминается это: Рамблинг на запечатанном ключевом слове

22 голосов
/ 21 февраля 2012

Обновление: Начиная с .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 в конфигурации сборки выпуска) выдает идентичный 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

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

22 голосов
/ 03 февраля 2011

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

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


Подробнее

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

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

// 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();

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

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

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

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


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

4 голосов
/ 20 ноября 2009

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

В некоторых случаях csc может выдавать код операции callvirt вместо кода операции call. Тем не менее, кажется, что эти случаи редки.

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

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

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

3 голосов
/ 03 августа 2010

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

Наиболее важные причины для меня:

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

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

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

Хотелось бы, чтобы Microsoft сделала «запечатанный» стандарт, а не «незапечатанный».

3 голосов
/ 15 октября 2008

<вне темы-декламация>

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

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

</ офф-тема-декламация>

3 голосов
/ 05 августа 2008

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

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

2 голосов
/ 01 октября 2011

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

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

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

2 голосов
/ 05 августа 2008

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

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

SSCLI (Ротор)
SSCLI: общая языковая инфраструктура общего источника

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

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