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

142

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

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

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

Помогите мне узнать :)

Вайбхав
источник
9
Я не думаю, что закрытые классы были предназначены для увеличения производительности. То, что они делают, может быть случайным. Кроме того, профилируйте свое приложение после его рефакторинга для использования закрытых классов и определите, стоит ли оно затраченных усилий. Ограничение расширяемости для создания ненужной микрооптимизации в конечном итоге обойдется вам дорого. Конечно, если вы профилировали, и это позволяет вам достичь своих показателей производительности (а не производительности ради производительности), тогда вы можете принять решение как команда, стоит ли оно потраченных денег. Если у вас есть закрытые классы по причинам, не связанным с производительностью, оставьте их :)
Мерлин Морган-Грэм
1
Вы пробовали с отражением? Я где-то читал, что создание экземпляров с помощью отражения быстрее с запечатанными классами
2010 г.,
Насколько мне известно, его нет. Sealed существует по другой причине - для блокировки расширяемости, которая может быть полезна / необходима во многих случаях. Оптимизация производительности здесь не была целью.
TomTom
... но если вы думаете о компиляторе: если ваш класс запечатан, вы знаете адрес метода, который вы вызываете в своем классе во время компиляции. Если ваш класс не запечатан, вы должны разрешить метод во время выполнения, потому что вам может потребоваться вызвать переопределение. Это, безусловно , будет незначительным, но я мог видеть , что там будет некоторая разница.
Дэн Пьюзи
Да, но, как отметила ОП, это не дает реальных выгод. Архитектурные различия могут быть гораздо более актуальными.
TomTom

Ответы:

61

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

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

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

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

Лассе В. Карлсен
источник
2
«бессвязная» ссылка интересна тем, что звучит как техническая безупречность, но на самом деле это чепуха. Прочтите комментарии к статье для получения дополнительной информации. Резюме: приведены 3 причины: управление версиями, производительность и безопасность / предсказуемость - [см. Следующий комментарий]
Стивен А. Лоу
1
[продолжение] Управление версиями применяется только тогда, когда нет подклассов, да, но расширьте этот аргумент до каждого класса, и вдруг у вас нет наследования, и угадайте, что, язык больше не объектно-ориентированный (а просто объектно-ориентированный)! [см. следующий]
Стивен А. Лоу
3
[продолжение] пример производительности - это шутка: оптимизация вызова виртуального метода; почему у запечатанного класса вообще должен быть виртуальный метод, если он не может быть разделен на подклассы? Наконец, аргумент безопасности / предсказуемости явно бессмысленен: «вы не можете использовать его, поэтому он безопасен / предсказуем». ЛОЛ!
Стивен А. Лоу
16
@ Стивен А. Лоу - Я думаю, что Джеффри Рихтер пытался сказать несколько окольным путем, что если вы оставите свой класс незапечатанным, вам нужно подумать о том, как производные классы могут / будут использовать его, и если у вас нет время или желание сделать это правильно, а затем запечатать его, так как это с меньшей вероятностью вызовет критические изменения в чужом коде в будущем. Это вовсе не чушь, это здравый смысл.
Грег Бич
6
Запечатанный класс может иметь виртуальный метод, поскольку он может быть производным от класса, который его объявляет. Когда вы позже объявляете переменную запечатанного класса-потомка и вызываете этот метод, компилятор может послать прямой вызов известной реализации, поскольку он знает, что это никак не может отличаться от того, что в известном - at-compile-time vtable для этого класса. Что касается запечатанных / незапечатанных, это другое обсуждение, и я согласен с причинами, по которым классы по умолчанию запечатаны.
Лассе В. Карлсен
144

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

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

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

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

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

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

Кэмерон МакФарланд
источник
5
AFAIK, используются следующие обстоятельства call: в ситуации new T().Method(), для structметодов, для невиртуальных вызовов virtualметодов (например, base.Virtual()) или для staticметодов. Везде использует callvirt.
porges 05
1
Эээ ... Я понимаю, что это устарело, но это не совсем верно ... большая победа с запечатанными классами - это когда JIT-оптимизатор может встроить вызов ... в этом случае запечатанный класс может быть огромной победой .
Брайан Кеннеди
5
Почему этот ответ неверен. Из журнала изменений Mono: «Оптимизация девиртуализации для закрытых классов и методов, улучшающая производительность pystone IronPython 2.0 на 4%. Другие программы могут ожидать аналогичного улучшения [Родриго].». Запечатанные классы могут улучшить производительность, но, как всегда, это зависит от ситуации.
Smilediver
1
@Smilediver Он может улучшить производительность, но только если у вас плохой JIT (хотя я не знаю, насколько хороши .NET JIT в настоящее время - раньше было довольно плохо в этом отношении). Например, Hotspot будет встроить виртуальные вызовы и при необходимости деоптимизировать позже - следовательно, вы оплачиваете дополнительные накладные расходы, только если вы фактически подклассифицируете класс (и даже тогда не обязательно).
Voo
1
-1 JIT не обязательно должен генерировать один и тот же машинный код для тех же кодов операций IL. Проверка нуля и виртуальный вызов - это ортогональные и отдельные шаги callvirt. В случае запечатанных типов JIT-компилятор все еще может оптимизировать часть callvirt. То же самое происходит, когда JIT-компилятор может гарантировать, что ссылка не будет нулевой.
правый фолд 01
31

Обновление. Начиная с .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

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

Орион Эдвардс
источник
Любая причина, по которой вы опубликовали этот ответ дважды, вместо того, чтобы закрыть другой как дубликат? Мне они кажутся действительными дубликатами, хотя это не моя территория.
Flexo
1
Что, если бы методы были длиннее (более 32 байтов кода IL), чтобы избежать встраивания и посмотреть, какая операция вызова будет использоваться? Если он встроен, вы не видите вызова, поэтому не можете судить о последствиях.
ygoe
Я сбит с толку: почему там, callvirtкогда эти методы изначально не виртуальные?
freakish
@freakish Я не могу вспомнить, где я это видел, но я читал, что среда CLR использовалась callvirtдля закрытых методов, потому что она все еще должна проверять объект на нуль перед вызовом вызова метода, и как только вы это учитываете, вы можете просто использовать callvirt. Чтобы удалить callvirtи просто перейти напрямую, им нужно либо изменить C #, чтобы разрешить, ((string)null).methodCall()как это делает C ++, либо им нужно будет статически доказать, что объект не является нулевым (что они могли бы сделать, но не позаботились)
Орион Эдвардс
1
Реквизиты для попытки докопаться до уровня машинного кода, но будьте очень осторожны, делая такие утверждения, как «это дает твердое доказательство того, что не может быть никакого улучшения производительности». Вы показали, что для одного конкретного сценария нет разницы в собственном выводе. Это одна точка данных, и нельзя предполагать, что она распространяется на все сценарии. Во-первых, ваши классы не определяют никаких виртуальных методов, поэтому виртуальные вызовы вообще не требуются.
Мэтт Крейг
24

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

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


Детали

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

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

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


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

эонил
источник
1
Процессоры класса Pentium напрямую предварительно выбирают косвенную отправку. Иногда по этой причине перенаправление указателя на функцию происходит быстрее, чем if (нераспознаваемое).
Джошуа
2
Одним из преимуществ невиртуальных или закрытых функций является то, что они могут быть встроены в большем количестве ситуаций.
CodesInChaos 04
5

<off-topic-rant>

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

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

</off-topic-rant>

Стивен А. Лоу
источник
38
Я не закрываю свои классы по соображениям производительности. Я запечатываю их по соображениям дизайна. Проектировать наследование сложно, и большую часть времени эти усилия будут потрачены впустую. Я полностью согласен с предоставлением интерфейсов - это гораздо лучшее решение для распечатывания классов.
Джон Скит,
6
Инкапсуляция обычно является лучшим решением, чем наследование. Чтобы взять ваш конкретный пример потока, перехват исключений потока нарушает принцип подстановки Лискова, потому что вы изменили задокументированное поведение класса Thread, поэтому, даже если вы могли бы унаследовать его, было бы неразумно говорить, что вы можете использовать SafeThread везде вы можете использовать Thread. В этом случае вам будет лучше инкапсулировать Thread в другой класс, который имеет другое задокументированное поведение, которое вы можете сделать. Иногда все запечатано для вашего же блага.
Грег Бич
1
@ [Greg Beech]: мнение, а не факт - возможность унаследовать от Thread, чтобы исправить ужасную ошибку в его дизайне, НЕ плохо ;-) И я думаю, что вы переоцениваете LSP - доказуемое свойство q (x) в данном случае «необработанное исключение разрушает программу», что не является «желательным свойством» :-)
Стивен А. Лоу
1
Нет, но у меня была своя доля дрянного кода, когда я позволял другим разработчикам злоупотреблять моим материалом, не запечатывая его или разрешая угловые случаи. Большая часть моего кода в настоящее время - это утверждения и другие вещи, связанные с контрактами. И я совершенно открыто говорю о том, что делаю это только для того, чтобы причинить боль в ***.
Тьюринг завершен
2
Поскольку мы здесь рассуждаем не по теме, точно так же, как вы ненавидите закрытые классы, я ненавижу проглатываемые исключения. Нет ничего хуже, чем когда что-то идет, а программа продолжается. JavaScript - мой любимый. Вы вносите изменения в какой-то код, и внезапное нажатие кнопки абсолютно ничего не делает. Большой! Еще одно - ASP.NET и UpdatePanel; серьезно, если мой обработчик кнопок бросает это большое дело, и он должен быть АВАРИЙ, так что я знаю, что есть что-то, что нужно исправить! Кнопка , которая ничего не делает более бесполезная , чем кнопки , которая воспитывает вынужденную экран!
Роман Старков
4

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

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

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

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

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

Джейсон Орендорф
источник
3

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

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

Карл Сегин
источник
Они должны , но это , кажется , что они не делают. Если бы в CLR была концепция ненулевых ссылочных типов, то запечатанный класс действительно был бы лучше, поскольку компилятор мог бы генерировать callвместо callvirt... Я бы тоже хотел ненулевые ссылочные типы по многим другим причинам ... вздох :-(
Орион Эдвардс
3

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

Для меня самые важные причины:

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

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

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

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

Полный Тьюринга
источник
Я считаю, что «опустить» (исключить) должно быть «испускать» (производить)?
user2864740
2

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

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

SSCLI (Rotor)
SSCLI: Общая языковая инфраструктура с общим исходным кодом

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

hitec
источник
Тест включал создание иерархии классов с некоторыми методами, которые выполняли фиктивную работу (в основном, манипуляции со строками). Некоторые из этих методов были виртуальными. Они звонили друг другу то тут, то там. Затем вызываем эти методы 100, 10000 и 100000 раз ... и измеряем прошедшее время. Затем запустите их, отметив классы как запечатанные. и снова измерения. Никакой разницы в них нет.
Vaibhav
2

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

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

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

Брайан Кеннеди
источник
Компилятор C # по-прежнему будет использовать callvirt(вызов виртуального метода) для методов экземпляра в запечатанных классах, потому что он по-прежнему должен выполнять для них проверку нулевого объекта. Что касается встраивания, CLR JIT может (и делает) встроенные вызовы виртуальных методов как для запечатанных, так и для незапечатанных классов ... так что да. Производительность - это миф.
Орион Эдвардс
1

Чтобы действительно увидеть их, вам нужно проанализировать код 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 составляет:

  • mov : имеет задержку в 1 цикл, и процессор может поддерживать 2,5 инструкции за цикл этого типа
  • cmp : имеет задержку в 1 цикл, и процессор может поддерживать 2 инструкции за цикл этого типа

Ссылка : https://gmplib.org/~tege/x86-timing.pdf

Это означает, что в идеале время, необходимое для вызова запечатанного метода, составляет 2 цикла, в то время как время, необходимое для вызова метода производного или базового класса, составляет 3 цикла.

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

Иван Порта
источник
-10

Запустите этот код, и вы увидите, что запечатанные классы в 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

РусланГ
источник
10
С этим есть пара проблем. Во-первых, вы не сбрасываете секундомер между первым и вторым тестами. Во-вторых, способ вызова метода означает, что все коды операций будут вызывать, а не callvirt, поэтому тип не имеет значения.
Кэмерон МакФарланд
1
RuslanG, Вы забыли вызвать watch.Reset () после запуска первого теста. o_O:]
Да, в приведенном выше коде есть ошибки. Опять же, кто может похвастаться тем, что никогда не допускает ошибок в коде? Но у этого ответа есть одна важная вещь лучше, чем у всех других: он пытается измерить рассматриваемый эффект. И этот ответ также содержит код, который вы все проверяете и [массовое] голосование против (интересно, почему необходимо массовое голосование против). В отличие от других ответов. На мой взгляд, это заслуживает уважения ... Также обратите внимание, что пользователь новичок, так что подход тоже не очень приветливый. Несколько простых исправлений, и этот код станет полезным для всех нас. Исправьте + поделитесь фиксированной версией, если осмелитесь
Роланд Пихлакас
Я не согласен с @RolandPihlakas. Как вы сказали, «кто может похвастаться, чтобы никогда не вносить ошибок в код». Ответ -> Нет, нет. Но дело не в этом. Дело в том, что это неверная информация, которая может ввести в заблуждение другого нового программиста. Многие люди могут легко упустить из виду, что секундомер не был сброшен. Они могли поверить, что контрольная информация верна. Разве это не вреднее? Кто-то, кто быстро ищет ответ, может даже не читать эти комментарии, скорее он / она увидит ответ и может поверить в это.
Эмран Хуссейн