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

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

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

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

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

14 ответов

Решение

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

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

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

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

Ответ - нет, закрытые занятия не дают результатов лучше, чем незапечатанные.

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

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

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

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

Звони и звони

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

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

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

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


подробности

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

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

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


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

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

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

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

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

Есть случаи, когда csc возможно, придется испускать callvirt код операции вместо call опкод. Тем не менее, кажется, что такие случаи редки.

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

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

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

Начиная с .NET 6.0, да.

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

Например, в этом коде из статьи MSDN:

      [Benchmark(Baseline = true)]
public int NonSealed() => _nonSealed.M() + 42;

[Benchmark]
public int Sealed() => _sealed.M() + 42;

public class BaseType
{
    public virtual int M() => 1;
}

public class NonSealedType : BaseType
{
    public override int M() => 2;
}

public sealed class SealedType : BaseType
{
    public override int M() => 2;
}

Тест «NonSealed» выполняется за 0,9837 нс, но метод «Sealed» занимает не больше времени, чем функция, которая просто возвращает постоянное значение. Это связано с постоянным сворачиванием.

Проверка типов запечатанных классов также имеет преимущества в производительности, как в этом коде из статьи MSDN:

      private object _o = "hello";

[Benchmark(Baseline = true)]
public bool NonSealed() => _o is NonSealedType;

[Benchmark]
public bool Sealed() => _o is SealedType;

public class NonSealedType { }
public sealed class SealedType { }

Проверка незапечатанного типа занимает ~1,76 нс, а проверка запечатанного типа — всего ~0,07 нс.

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

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

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

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

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

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

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

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

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

Согласно отчету, написанному Торбьорном Гранлундом, "Задержки выполнения инструкций и пропускная способность для процессоров AMD и Intel x86", скорость выполнения следующей инструкции в Intel Pentium 4 составляет:

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

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

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

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

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

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

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

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

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

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

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

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

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

Да, использование запечатанных классов может помочь повысить производительность. (Здесь я имею в виду запечатанные классы, которые сами еще не наследуют от другого класса и не реализуют интерфейс.) Основной принцип заключается в том, что избегание уровней косвенности при вызове функций повышает производительность. Эта косвенность обычно связана с виртуальными вызовами и/или виртуальными таблицами.

См. раздел «Девиртуализация» в этом разделе JIT об улучшениях, доступных в .NET 6: https://devblogs.microsoft.com/dotnet/ Performance-improvements-in-net-6/#jit.

https://learn.microsoft.com/en-us/dotnet/fundamentals/code-anaанализ/quality-rules/ca1852

https://learn.microsoft.com/en-us/dotnet/fundamentals/code-anaлиз/quality-rules/ca1859

См. также: используйте ограниченный универсальный вариант для повышения производительности:
/questions/41676869/kak-izbezhat-nakladnyih-rashodov-na-virtualnyie-vyizovyi-c/41676882#41676882


Боковое примечание (необязательное чтение):

Как и в C#, в C++ предпочтение отдается конкретным объектам. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#SS-concrete

В C# ограничения «где» для структур, реализующих интерфейсы, могут быть полезны, чтобы избежать влияния на производительность, связанного с наследованием (реализацией) интерфейса. А для C++ ограничения «требования» (концепции C++) для параметров шаблона могут быть полезны, чтобы вообще избежать использования наследования.
C++20 лучший способ конвертировать абстрактный класс (интерфейс) и примеси в концепцию

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

Другие вопросы по тегам