Чи справді закриті заняття пропонують переваги від продуктивності?


140

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

Я провів кілька тестів, щоб перевірити різницю продуктивності та не знайшов жодного. Чи я щось роблю не так? Я пропускаю випадок, коли закриті заняття дадуть кращі результати?

Хтось запускав тести і бачив різницю?

Допоможи мені навчитися :)


9
Я не думаю, що закриті заняття не мали на меті збільшити підвищення продуктивності. Те, що вони роблять, може бути випадковим. На додаток до цього, профіліруйте додаток після того, як ви його реконструювали, щоб використовувати герметичні класи, і визначте, чи варто докладати зусиль. Блокування розширення для здійснення непотрібної мікрооптимізації у довгостроковій перспективі обійдеться вам. Звичайно, якщо ви профайлювались, і це дозволяє вам досягти ваших перфектних орієнтирів (а не perf заради perf), тоді ви можете прийняти рішення як команда, якщо на це варто витратити гроші. Якщо ви запечатали заняття з неприйнятих причин, тоді зберігайте їх :)
Мерлін Морган-Грем

1
Ви пробували з роздумом? Я де - то читав , що інстанцірованія відображення швидше з запечатаними класами
onof

Наскільки мені відомо, його немає. Запечатано там з іншої причини - блокувати розширюваність, що може бути корисним / потрібним у багатьох випадках. Оптимізація продуктивності тут не була метою.
TomTom

... але якщо ви думаєте про компілятор: якщо ваш клас запечатаний, ви знаєте адресу методу, який ви викликаєте у свій клас під час компіляції. Якщо ваш клас не запечатаний, вам доведеться вирішити метод під час виконання, тому що вам може знадобитися викликати переопрацювання. Це, безумовно, буде незначним, але я міг бачити, що буде якась різниця.
Dan Puzey

Так, але це не призводить до реальних вигод, як вказувала ОП. Архітектурні відмінності є / можуть бути набагато актуальнішими.
TomTom

Відповіді:


60

JITter іноді використовуватиме невіртуальні дзвінки до методів у закритих класах, оскільки немає можливості їх продовжити.

Існують складні правила щодо типу виклику, віртуального / невіртуального, і я не знаю їх усіх, тому я не можу насправді окреслити їх для вас, але якщо ви перебуваєте в Google для закритих класів і віртуальних методів, ви можете знайти деякі статті з цієї теми.

Зауважте, що будь-які переваги від продуктивності, які ви отримали б від цього рівня оптимізації, слід розглядати як крайній варіант, завжди оптимізуйте на алгоритмічному рівні, перш ніж оптимізувати його на рівні коду.

Ось одне посилання, що згадує про це: Рамбування за запечатаним ключовим словом


2
посилання "невмілий" цікавий тим, що це звучить як технічна користь, але насправді є дурницею. Прочитайте коментарі до статті для отримання додаткової інформації. Короткий зміст: 3 наведені причини: Версія, продуктивність та безпека / передбачуваність - [див. Наступний коментар]
Стівен А. Лоу

1
[продовження] Версія застосовується лише тоді, коли підкласів немає, так, але поширюйте цей аргумент на кожен клас і раптом у вас немає спадку і здогадуєтесь про що, мова вже не орієнтована на об'єкти (а лише на об'єкті)! [див. далі]
Стівен А. Лоу

3
[продовження] Приклад продуктивності - жарт: оптимізація виклику віртуального методу; чому б у запечатаному класі в першу чергу був віртуальний метод, оскільки його не можна підкласифікувати? Нарешті, аргумент безпеки / передбачуваності явно жирний: "ви не можете використовувати його, щоб він був безпечним / передбачуваним". ЛОЛ!
Стівен А. Лоу

16
@Steven A. Lowe - Я думаю, що Джеффрі Ріхтер намагався сказати дещо крутим способом - це те, що якщо ви залишаєте клас незапечатаним, вам потрібно подумати про те, як похідні класи можуть / як його використовуватимуть, а якщо у вас немає час або схильність робити це належним чином, а потім закріпіть його, оскільки це менше шансів призвести до порушення змін у коді інших людей у ​​майбутньому. Це зовсім не дурниця, це здоровий глузд.
Грег Бук

6
Запечатаний клас може мати віртуальний метод, оскільки він може походити від класу, який оголошує його. Коли ви пізніше оголосите змінну закритого класу нащадків і зателефонуєте цьому методу, компілятор може надіслати прямий виклик відомої реалізації, оскільки знає, що немає способу, щоб це могло бути інакше, ніж те, що у відомому- vtable-time vtable для цього класу. Що стосується запечатаного / незапечатаного, то це вже інше обговорення, і я погоджуюся з причинами того, щоб зробити класи запечатаними за замовчуванням.
Лассе В. Карлсен

143

Відповідь ні, запечатані заняття не працюють краще, ніж незапечатані.

Проблема зводиться до кодів callvs vs callvirtIL. Callшвидше, ніж callvirt, і callvirtв основному використовується, коли ви не знаєте, чи був об'єкт підкласом. Тож люди припускають, що якщо ви запечатаєте клас, всі оп-коди зміняться з calvirtsна callsта стануть швидшими.

На жаль callvirt, інші речі, які також роблять його корисним, як перевірка нульових посилань. Це означає, що навіть якщо клас запечатаний, посилання може все-таки бути нульовим, і, отже callvirt, потрібно. Ви можете обійти це (не потребуючи закріплення класу), але це стає трохи безглуздо.

Структури використовують, callтому що вони не можуть бути підкласами і ніколи не бувають нульовими.

Дивіться це запитання для отримання додаткової інформації:

Дзвінок і виклик


5
AFAIK, обставини call, що застосовуються: в ситуації new T().Method(), для structметодів, для невіртуальних викликів virtualметодів (таких як base.Virtual()), або для staticметодів. Всюди користується callvirt.
porges

1
Ага ... я усвідомлюю, що це старе, але це не зовсім правильно ... велика виграш із закритими класами - це коли Оптимізатор JIT може вбудовувати виклик ... у цьому випадку герметичний клас може стати величезною виграшею .
Брайан Кеннеді

5
Чому ця відповідь неправильна. З журналу Mono change: "Оптимізація девіртуалізації для запечатаних класів та методів, покращивши продуктивність IronPython 2.0 pystone на 4%. Інші програми можуть очікувати подібного поліпшення [Rodrigo].". Запечатані класи можуть покращити продуктивність, але, як завжди, це залежить від ситуації.
Smilediver

1
@Smilediver Це може підвищити продуктивність, але тільки якщо у вас поганий JIT (навіть не уявляю, наскільки хороші .NET JIT сьогодні, хоча раніше - досить погано). Наприклад, точка вказує вбудовані віртуальні дзвінки та при необхідності деоптимізуватиметься пізніше - отже, ви сплатите додаткові накладні витрати лише у тому випадку, якщо ви насправді підкласируєте клас (і навіть тоді це не обов'язково).
Во

1
-1 JIT не обов'язково повинен генерувати один і той же машинний код для тих же IL-кодів. Перевірка нуля та віртуальний виклик є ортогональними та окремими кроками callvirt. У випадку з герметичними типами компілятор JIT все ще може оптимізувати частину callvirt. Те саме відбувається, коли компілятор JIT може гарантувати, що посилання не буде нульовим.
праворуч

29

Оновлення: Станом на .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 

Часто цитується причина, за якою люди кажуть, що запечатаний приносить переваги продуктивності - це те, що компілятор знає, що клас не перекрито, і тому він може використовувати callзамість того callvirt, що він не повинен перевіряти віртуальність тощо. Як було доведено вище, це не так правда.

Наступна моя думка полягала в тому, що, хоча MSIL ідентичний, можливо, компілятор JIT трактує класи, запечатані по-різному?

Я провів збірку релізів під налагоджувачем візуальної студії та переглянув розкладений вихід 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 для прориву після завершення програми та перегляду розбирання компільованого кодом x86 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

Для мене це є надійним доказом того, що між методами виклику на закритих та незапечатаних класах не може бути покращено продуктивність ... Я думаю, що я щасливий зараз :-)


Будь-яка причина, чому ви двічі опублікували цю відповідь, а не закривали іншу як дублікат? Вони виглядають як дійсні дублікати для мене, хоча це не моя область.
Flexo

1
Що робити, якщо методи були довші (більше 32 байт коду IL), щоб уникнути вбудованої інформації та побачити, яка операція виклику буде використовуватися? Якщо він вбудований, ви не бачите дзвінка, тому не можете судити про його ефекти.
ygoe

Мене бентежить: чому там, callvirtколи ці методи не є віртуальними для початку?
фрік

@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 класу попередньо вибирають непряму доставку безпосередньо. Іноді перенаправлення покажчиків функцій відбувається швидше, ніж якби з цієї причини (нездійсненне).
Джошуа

2
Однією з переваг невіртуальних або герметичних функцій є те, що вони можуть бути окреслені в більшій кількості ситуацій.
CodesInChaos

4

<off-topic-rant>

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

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

</off-topic-rant>


35
Я не печу мої заняття з міркувань продуктивності. Я запечатую їх з міркувань дизайну. Проектування для успадкування важко, і ці зусилля будуть витрачені даремно. Я повністю згоден щодо надання інтерфейсів - це набагато чудове рішення для розблокування класів.
Джон Скіт

6
Інкапсуляція, як правило, краще рішення, ніж успадкування. Для прикладу конкретного потоку, виняток вилучення ниток порушує Принцип заміщення Ліскова, оскільки ви змінили задокументовану поведінку класу Thread, тому навіть якщо ви могли з цього вийти, було б нерозумно говорити, що ви можете використовувати SafeThread скрізь ви можете використовувати Thread. У цьому випадку вам краще буде інкапсулювати Нитку в інший клас, який має інше документоване поведінку, яке ви вмієте робити. Іноді речі запечатуються на ваше власне благо.
Грег Бук

1
@ [Грег Бук]: думка, а не факт - спроможність успадкувати від Thread виправити надзвичайний нагляд за її дизайном НЕ погана річ ;-) І я думаю, що ви завищуєте LSP - доказову властивість q (x) у цей випадок - це "необроблений виняток знищує програму", яка не є "бажаною властивістю" :-)
Стівен А. Лоу

1
Ні, але я мав свою частку шаленого коду, де я дав змогу іншим розробникам зловживати моїми речами, не запечатуючи їх або дозволяючи кутові корпуси. Більшість мого коду сьогодні - це твердження та інші речі, пов'язані з контрактом. І я досить відкритий щодо того, що роблю це лише для того, щоб боліти ***.
Повне Тюрінг

2
Оскільки ми тут робимо тематичні рейтинги, так само, як ти ненавидить герметичні заняття, я ненавиджу проковтнути винятки. Немає нічого гіршого, ніж коли щось йде titsup, але програма продовжується. JavaScript - мій улюблений. Ви вносите зміни в якийсь код і раптом натискання кнопки абсолютно нічого не робить. Чудово! ASP.NET та UpdatePanel - ще один; серйозно, якщо мій обробник кнопок кидає це велика угода, і це потрібно РОЗКРИТИ, так що я знаю, що є щось, що потребує виправлення! Кнопка, яка нічого не робить, є більш марною, ніж кнопка, яка виводить з екран екрана!
Роман Старков

4

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

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

І мені здається, що JIT повинен бути в змозі випромінювати той самий невіртуальний виклик функції для callvirtтого, що був би call, якщо він знає, що в класі ще немає підкласів (поки). Якщо існує лише одна реалізація методу, немає сенсу завантажувати його адресу з vtable - просто викличте одну реалізацію безпосередньо. З цього питання JIT навіть може вбудувати функцію.

Це трохи азартної гри на частини ЛТ, тому що якщо підклас буде пізніше завантажений, JIT доведеться викинути , що машинний код і скомпілювати код ще раз, випускаючи реальний віртуальний виклик. Я здогадуюсь, це на практиці трапляється не часто.

(І так, дизайнери VM дійсно агресивно переслідують ці крихітні виграші у виконанні.)


3

Запечатані класи повинні забезпечити підвищення продуктивності. Оскільки герметичний клас неможливо отримати, будь-які віртуальні члени можуть бути перетворені на невіртуальних членів.

Звичайно, ми говоримо про дійсно невеликі вигоди. Я б не відзначив клас запечатаним лише для поліпшення продуктивності, якщо тільки профілювання не виявило, що це є проблемою.


Вони повинні , але, схоже, цього не роблять. Якби CLR мав поняття ненульових типів посилань, то герметичний клас дійсно був би кращим, оскільки компілятор міг би випускати callзамість callvirt... Я б хотів і ненульових типів посилань з багатьох інших причин ... зітхання :-(
Оріон Едвардс

3

Я вважаю "запечатані" класи нормальним випадком, і я ВИНАГА маю підстави опустити "запечатане" ключове слово.

Найбільш важливі причини для мене:

a) Кращі перевірки часу компіляції (кастинг на інтерфейси, які не реалізовані, буде виявлено під час компіляції, не тільки під час виконання)

і, головна причина:

б) Зловживання моїми заняттями не можливе таким чином

Я б хотів, щоб Microsoft зробила «запечатаний» стандарт, а не «незапечатаний».


Я вважаю, що «опустити» (залишити поза) слід «випромінювати» (виробляти)?
користувач2864740

2

@Vaibhav, які тести ви виконували для вимірювання продуктивності?

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

SSCLI (Rotor)
SSCLI: Загальна мовна інфраструктура спільного джерела

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


Тест включав створення ієрархії класів, з деякими методами, які виконували фіктивну роботу (в основному, маніпуляції з рядками). Деякі з цих методів були віртуальними. Вони дзвонили один одному тут і там. Потім викликайте ці методи в 100, 10000 і 100000 разів ... і вимірюйте час, що минув. Потім запускайте їх після позначення класів запечатаними. і знову вимірювання. Різниці в них немає.
Вайбхав

2

Запечатані класи будуть принаймні крихітними, але іноді вони можуть бути waayyy швидше ... якщо Оптимізатор JIT може вбудовувати дзвінки, які інакше були б віртуальними дзвінками. Отже, там, де часто називаються методи, які є досить маленькими, щоб їх можна було окреслити, обов'язково врахуйте герметичність класу.

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

Я знаю, що деякі з них казали, що ненавидять запечатані класи, тому що вони хочуть отримати можливість з чого-небудь ... але це часто НЕ найвигідніший вибір ... тому що, коли клас класти на деривацію, ви заблокуєте вас набагато більше, ніж не виставляючи всіх що. Це схоже на висловлювання "Я ненавиджу класи, які мають приватних членів ... Я часто не можу змусити клас робити те, що я хочу, бо не маю доступу". Інкапсуляція важлива ... герметизація - одна з форм інкапсуляції.


Компілятор C # все ще використовуватиме callvirt(виклик віртуального методу), наприклад, методів у закритих класах, оскільки він все ще повинен перевірити нульовий об'єкт на них. Що стосується вбудовування, CLR JIT може (і робить) вбудований віртуальний метод вимагає як закритих, так і незапечатаних класів ... так, так. Перформанс - це міф.
Оріон Едвардс

1

Щоб їх дійсно побачити, вам потрібно проаналізувати тріскування, складене 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), а потім викликайте метод, тоді як похідний / базовий клас виконує безпосередньо метод. .

Відповідно до звіту, написаного Torbj¨orn 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";
    }
}

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


9
З цим є пара проблем. По-перше, ви не скидаєте секундомір між першим і другим тестом. По-друге, спосіб виклику методу означає, що всі оп коди будуть викликом не callvirt, тому тип не має значення.
Камерон Макфарланд

1
RuslanG, ти забув зателефонувати на watch.Reset () після запуску першого тесту. o_O:]

Так, у наведеному вище коді є помилки. Тоді знову, хто може похвалитися собою, щоб ніколи не вводити помилок у код? Але ця відповідь має одну важливу річ кращу, ніж усі інші: вона намагається виміряти відповідний ефект. І ця відповідь також ділиться кодом для всіх вас, хто перевіряє та [mass-] downvote (мені цікаво, чому потрібне масове знищення?). На відміну від інших відповідей. На мою думку, це заслуговує на повагу ... Також зверніть увагу, що користувач є новачком, тому не дуже привітний підхід. Деякі прості виправлення, і цей код стає корисним для всіх нас. Виправити + поділитися фіксованою версією, якщо ви наважуєтесь
Roland Pihlakas

Я не згоден з @RolandPihlakas. Як ви сказали, "хто може похвалитися собою, щоб ніколи не вводити помилки в код". Відповідь -> Ні, ні. Але це не сенс. Справа в тому, що це неправильна інформація, яка може ввести в оману іншого нового програміста. Багато людей можуть легко пропустити, щоб секундомір не був скинутий. Вони могли повірити, що інформація про орієнтир правдива. Хіба це не шкідливіше? Хтось, хто швидко шукає відповідь, може навіть не прочитати ці коментарі, скоріше він / вона побачить відповідь, і він / вона може повірити в це.
Емран Хуссей
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.