Вопрос:

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

.net optimization frameworks performance

23281 просмотра

11 ответа

8692 Репутация автора

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

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

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

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

Автор: Vaibhav Источник Размещён: 05.08.2008 12:00

Ответы (11)


52 плюса

282321 Репутация автора

Решение

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

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

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

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

Автор: Lasse Vågsæther Karlsen Размещён: 05.08.2008 12:32

3 плюса

17009 Репутация автора

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

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

Автор: Karl Seguin Размещён: 05.08.2008 12:37

2 плюса

965 Репутация автора

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

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

SSCLI (ротор)
SSCLI: общая исходная инфраструктура общего источника

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

Автор: hitec Размещён: 05.08.2008 12:40

3 плюса

52731 Репутация автора

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

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

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

Автор: Steven A. Lowe Размещён: 14.10.2008 08:04

136 плюса

51518 Репутация автора

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

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

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

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

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

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

Автор: Cameron MacFarland Размещён: 24.12.2008 02:09

4 плюса

30075 Репутация автора

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

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

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

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

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

Автор: Jason Orendorff Размещён: 20.11.2009 09:53

-9 плюса

17 Репутация автора

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

output: Sealed class: 00: 00: 00.1897568 Класс NonSealed: 00: 00: 00.3826678

Автор: RuslanG Размещён: 26.11.2009 05:50

3 плюса

644 Репутация автора

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

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

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

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

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

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

Автор: Turing Complete Размещён: 03.08.2010 02:27

23 плюса

38820 Репутация автора

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

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


подробности

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

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

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


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

Автор: Eonil Размещён: 03.02.2011 03:22

2 плюса

1651 Репутация автора

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

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

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

Автор: Brian Kennedy Размещён: 01.10.2011 10:37

20 плюса

83887 Репутация автора

Обновление. Начиная с .NET Core 2.0 и .NET Desktop 4.7.1, CLR теперь поддерживает девиртуализацию. Он может принимать методы в закрытых классах и заменять виртуальные вызовы прямыми вызовами - и он также может делать это для незапечатанных классов, если он может понять, что это безопасно.

В таком случае (закрытый класс, который CLR не может обнаружить иначе как безопасный для devirtualise), запечатанный класс должен фактически предлагать какое-то преимущество в производительности.

Тем не менее, я бы не думаю , что было бы стоит беспокоиться , если вы не уже профилированным код и определили , что вы были в особенно жарком пути называют миллионы раз, или что - то подобное:

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 

Часто цитируемая причина, по которой люди говорят, что это запечатанные, обеспечивает преимущества в производительности, заключается в том, что компилятор знает, что класс не переоценивается и, следовательно, может использоваться 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-кода JIT.

Как видно из приведенного ниже кода, при работе за пределами отладчика 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

Vs запечатанный класс:

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

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

Автор: Orion Edwards Размещён: 20.02.2012 08:45
32x32