Я наткнулся на множество советов по оптимизации, в которых говорится, что вы должны отмечать свои классы как запечатанные, чтобы получить дополнительные преимущества в производительности.
Я провел несколько тестов, чтобы проверить разницу в производительности, и ничего не нашел. Я делаю что-то неправильно? Я упускаю из виду тот случай, когда закрытые классы дадут лучшие результаты?
Кто-нибудь запускал тесты и видел разницу?
Помогите мне узнать :)
.net
optimization
frameworks
performance
Вайбхав
источник
источник
Ответы:
JITter иногда будет использовать невиртуальные вызовы методов в запечатанных классах, поскольку их нельзя расширить дальше.
Существуют сложные правила относительно типа вызова, виртуальный / невиртуальный, и я не знаю их всех, поэтому я не могу описать их для вас, но если вы Google для запечатанных классов и виртуальных методов, вы можете найти несколько статей по этой теме.
Обратите внимание, что любое повышение производительности, которое вы можете получить от этого уровня оптимизации, следует рассматривать как последнее средство, всегда оптимизируйте на алгоритмическом уровне, прежде чем оптимизировать на уровне кода.
Вот одна ссылка, в которой упоминается об этом: бессмысленное использование ключевого слова sealed
источник
Ответ - нет, закрытые классы не работают лучше, чем незапечатанные.
Проблема сводится к кодам
call
операций vscallvirt
IL.Call
быстрее, чемcallvirt
, иcallvirt
в основном используется, когда вы не знаете, был ли объект подклассифицирован. Поэтому люди предполагают, что если вы запечатаете класс, все коды операций изменятся сcalvirts
наcalls
и будут быстрее.К сожалению,
callvirt
делает и другие вещи, которые делают его полезным, например, проверку на пустые ссылки. Это означает, что даже если класс запечатан, ссылка все равно может быть нулевой и, следовательно,callvirt
необходим a . Вы можете обойти это (без необходимости запечатывать класс), но это становится бессмысленным.Структуры используются,
call
потому что они не могут быть подклассами и никогда не имеют значения NULL.См. Этот вопрос для получения дополнительной информации:
Звоните и звоните
источник
call
: в ситуацииnew T().Method()
, дляstruct
методов, для невиртуальных вызововvirtual
методов (например,base.Virtual()
) или дляstatic
методов. Везде использует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) выдает идентичный 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
метод прямо в вызывающую программу. Однако важно то, что он был идентичен при вызове класса sealed vs non-sealed. Нет никакой разницы между закрытым и незапечатанным классом.Вот это при вызове обычного класса:
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
Для меня это убедительное доказательство того, что не может быть никакого улучшения производительности между вызовами методов в запечатанных и незапечатанных классах ... Думаю, теперь я счастлив :-)
источник
callvirt
когда эти методы изначально не виртуальные?callvirt
для закрытых методов, потому что она все еще должна проверять объект на нуль перед вызовом вызова метода, и как только вы это учитываете, вы можете просто использоватьcallvirt
. Чтобы удалитьcallvirt
и просто перейти напрямую, им нужно либо изменить C #, чтобы разрешить,((string)null).methodCall()
как это делает C ++, либо им нужно будет статически доказать, что объект не является нулевым (что они могли бы сделать, но не позаботились)Насколько я знаю, нет никаких гарантий повышения производительности. Но есть шанс снизить потери производительности при определенных условиях с помощью герметичного метода. (запечатанный класс делает все методы запечатанными.)
Но это зависит от реализации компилятора и среды выполнения.
Детали
Многие современные процессоры используют длинную конвейерную структуру для повышения производительности. Поскольку ЦП невероятно быстрее памяти, ЦП должен предварительно выбирать код из памяти для ускорения конвейера. Если код не будет готов вовремя, конвейеры будут простаивать.
Существует большое препятствие, называемое динамической диспетчеризацией, которое нарушает оптимизацию «предварительной выборки». Вы можете понять это как просто условное ветвление.
// 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
.Этот ответ ( Почему быстрее обрабатывать отсортированный массив, чем несортированный? ) Намного лучше описывает прогнозирование ветвления.
источник
<off-topic-rant>
Я ненавижу закрытые классы. Даже если преимущества в производительности поразительны (в чем я сомневаюсь), они разрушают объектно-ориентированную модель, предотвращая повторное использование через наследование. Например, класс Thread запечатан. Хотя я вижу, что потоки могут быть как можно более эффективными, я также могу представить себе сценарии, в которых возможность создания подкласса Thread будет иметь большие преимущества. Авторы классов, если вы должны запечатать свои классы из соображений "производительности", пожалуйста, предоставьте интерфейс, по крайней мере, чтобы нам не приходилось обертывать и заменять везде, где нам нужна функция, которую вы забыли.
Пример: SafeThread должен был обернуть класс Thread, потому что Thread запечатан и интерфейса IThread нет; SafeThread автоматически перехватывает необработанные исключения в потоках, чего полностью не хватает в классе Thread. [и нет, необработанные события исключения не собирают необработанные исключения во вторичных потоках].
</off-topic-rant>
источник
Маркировка класса не
sealed
должна влиять на производительность.Бывают случаи, когда,
csc
возможно, придется выдатьcallvirt
код операции вместоcall
кода операции. Однако, похоже, такие случаи редки.И мне кажется, что JIT должен иметь возможность генерировать тот же вызов невиртуальной функции, для
callvirt
которого он был быcall
, если он знает, что у класса нет подклассов (пока). Если существует только одна реализация метода, нет смысла загружать его адрес из vtable - просто вызовите эту реализацию напрямую. В этом отношении JIT может даже встроить функцию.Это немного азартной игры на части ЛТ, потому что если подкласс будет позже загружен, JIT придется выбросить , что машинный код и скомпилировать код еще раз, испуская реальный виртуальный вызов. Думаю, на практике такое случается нечасто.
(И да, дизайнеры виртуальных машин действительно настойчиво добиваются этих крошечных выигрышей в производительности.)
источник
Запечатанные классы должны обеспечивать повышение производительности. Поскольку запечатанный класс не может быть производным, любые виртуальные члены можно превратить в невиртуальные члены.
Конечно, речь идет о действительно небольших выигрышах. Я бы не стал отмечать класс как запечатанный только для повышения производительности, если профилирование не выявило проблему.
источник
call
вместоcallvirt
... Я бы тоже хотел ненулевые ссылочные типы по многим другим причинам ... вздох :-(Я считаю «запечатанные» классы нормальным случаем, и у меня ВСЕГДА есть причина опустить ключевое слово «запечатанные».
Для меня самые важные причины:
a) Лучшая проверка времени компиляции (приведение к нереализованным интерфейсам будет обнаружено во время компиляции, а не только во время выполнения)
и главная причина:
б) Таким образом, злоупотребление моими занятиями невозможно
Я хотел бы, чтобы Microsoft сделала стандарт «запечатанным», а не «распечатанным».
источник
@Vaibhav, какие тесты вы выполняли для измерения производительности?
Я предполагаю, что нужно было бы использовать Rotor и углубиться в CLI и понять, как запечатанный класс может улучшить производительность.
источник
запечатанные классы будут, по крайней мере, немного быстрее, но иногда могут быть намного быстрее ... если оптимизатор JIT может встроить вызовы, которые в противном случае были бы виртуальными вызовами. Итак, если есть часто вызываемые методы, которые достаточно малы для встраивания, определенно подумайте о том, чтобы запечатать класс.
Однако лучшая причина для запечатывания класса - это сказать: «Я не проектировал это для наследования от него, поэтому я не позволю вам обжечься, предполагая, что он был так разработан, и я не собираюсь чтобы сжечь себя, будучи заблокированным в реализации, потому что я позволил вам извлечь из нее. "
Я знаю, что некоторые здесь говорят, что ненавидят закрытые классы, потому что им нужна возможность наследовать от чего угодно ... но это ЧАСТО не самый удобный в обслуживании выбор ... потому что раскрытие класса производным блокирует вас намного больше, чем не раскрытие всего что. Это похоже на высказывание «Я ненавижу классы, у которых есть частные члены ... Я часто не могу заставить класс делать то, что хочу, потому что у меня нет доступа». Герметизация важна ... герметизация - одна из форм герметизации.
источник
callvirt
(вызов виртуального метода) для методов экземпляра в запечатанных классах, потому что он по-прежнему должен выполнять для них проверку нулевого объекта. Что касается встраивания, CLR JIT может (и делает) встроенные вызовы виртуальных методов как для запечатанных, так и для незапечатанных классов ... так что да. Производительность - это миф.Чтобы действительно увидеть их, вам нужно проанализировать код e, скомпилированный JIT (последний).
Код C #
public sealed class Sealed { public string Message { get; set; } public void DoStuff() { } } public class Derived : Base { public sealed override void DoStuff() { } } public class Base { public string Message { get; set; } public virtual void DoStuff() { } } static void Main() { Sealed sealedClass = new Sealed(); sealedClass.DoStuff(); Derived derivedClass = new Derived(); derivedClass.DoStuff(); Base BaseClass = new Base(); BaseClass.DoStuff(); }
Код MIL
.method private hidebysig static void Main() cil managed { .entrypoint // Code size 41 (0x29) .maxstack 8 IL_0000: newobj instance void ConsoleApp1.Program/Sealed::.ctor() IL_0005: callvirt instance void ConsoleApp1.Program/Sealed::DoStuff() IL_000a: newobj instance void ConsoleApp1.Program/Derived::.ctor() IL_000f: callvirt instance void ConsoleApp1.Program/Base::DoStuff() IL_0014: newobj instance void ConsoleApp1.Program/Base::.ctor() IL_0019: callvirt instance void ConsoleApp1.Program/Base::DoStuff() IL_0028: ret } // end of method Program::Main
JIT-скомпилированный код
--- C:\Users\Ivan Porta\source\repos\ConsoleApp1\Program.cs -------------------- { 0066084A in al,dx 0066084B push edi 0066084C push esi 0066084D push ebx 0066084E sub esp,4Ch 00660851 lea edi,[ebp-58h] 00660854 mov ecx,13h 00660859 xor eax,eax 0066085B rep stos dword ptr es:[edi] 0066085D cmp dword ptr ds:[5842F0h],0 00660864 je 0066086B 00660866 call 744CFAD0 0066086B xor edx,edx 0066086D mov dword ptr [ebp-3Ch],edx 00660870 xor edx,edx 00660872 mov dword ptr [ebp-48h],edx 00660875 xor edx,edx 00660877 mov dword ptr [ebp-44h],edx 0066087A xor edx,edx 0066087C mov dword ptr [ebp-40h],edx 0066087F nop Sealed sealedClass = new Sealed(); 00660880 mov ecx,584E1Ch 00660885 call 005730F4 0066088A mov dword ptr [ebp-4Ch],eax 0066088D mov ecx,dword ptr [ebp-4Ch] 00660890 call 00660468 00660895 mov eax,dword ptr [ebp-4Ch] 00660898 mov dword ptr [ebp-3Ch],eax sealedClass.DoStuff(); 0066089B mov ecx,dword ptr [ebp-3Ch] 0066089E cmp dword ptr [ecx],ecx 006608A0 call 00660460 006608A5 nop Derived derivedClass = new Derived(); 006608A6 mov ecx,584F3Ch 006608AB call 005730F4 006608B0 mov dword ptr [ebp-50h],eax 006608B3 mov ecx,dword ptr [ebp-50h] 006608B6 call 006604A8 006608BB mov eax,dword ptr [ebp-50h] 006608BE mov dword ptr [ebp-40h],eax derivedClass.DoStuff(); 006608C1 mov ecx,dword ptr [ebp-40h] 006608C4 mov eax,dword ptr [ecx] 006608C6 mov eax,dword ptr [eax+28h] 006608C9 call dword ptr [eax+10h] 006608CC nop Base BaseClass = new Base(); 006608CD mov ecx,584EC0h 006608D2 call 005730F4 006608D7 mov dword ptr [ebp-54h],eax 006608DA mov ecx,dword ptr [ebp-54h] 006608DD call 00660490 006608E2 mov eax,dword ptr [ebp-54h] 006608E5 mov dword ptr [ebp-44h],eax BaseClass.DoStuff(); 006608E8 mov ecx,dword ptr [ebp-44h] 006608EB mov eax,dword ptr [ecx] 006608ED mov eax,dword ptr [eax+28h] 006608F0 call dword ptr [eax+10h] 006608F3 nop } 0066091A nop 0066091B lea esp,[ebp-0Ch] 0066091E pop ebx 0066091F pop esi 00660920 pop edi 00660921 pop ebp 00660922 ret
Хотя создание объектов такое же, инструкции, выполняемые для вызова методов запечатанного и производного / базового классов, немного отличаются. После перемещения данных в регистры или ОЗУ (инструкция mov), вызова запечатанного метода, выполняется сравнение между dword ptr [ecx], ecx (инструкция cmp), а затем вызывается метод, в то время как производный / базовый класс выполняет непосредственно метод. .
Согласно отчету, написанному Торбьорном Гранлундом (Torbjstructionorn Granlund), Задержки выполнения инструкций и пропускная способность для процессоров AMD и Intel x86 , скорость выполнения следующей инструкции в Intel Pentium 4 составляет:
Ссылка : https://gmplib.org/~tege/x86-timing.pdf
Это означает, что в идеале время, необходимое для вызова запечатанного метода, составляет 2 цикла, в то время как время, необходимое для вызова метода производного или базового класса, составляет 3 цикла.
Оптимизация компиляторов привела к тому, что разница между производительностью запечатанного и незапечатанного классов настолько низка, что мы говорим о кругах процессора и по этой причине не имеют отношения к большинству приложений.
источник
Запустите этот код, и вы увидите, что запечатанные классы в 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"; } }
вывод: класс Sealed: 00: 00: 00.1897568 NonSealed класс: 00: 00: 00.3826678
источник