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

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

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

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

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

5.08.2008 12:00:28
Я не думаю, что закрытые уроки были предназначены для увеличения производительности. Тот факт, что они делают, может быть случайным. Кроме того, профилируйте свое приложение после того, как вы реорганизовали его для использования закрытых классов, и определите, стоило ли оно усилий. Блокировка вашей расширяемости для ненужной микрооптимизации обойдется вам в долгосрочной перспективе. Конечно, если вы профилировали, и это позволяет вам достигать своих тестов производительности (а не перфорирования ради перфекта), то вы можете принять решение как команда, если это стоит потраченных денег. Если у вас есть запечатанные уроки по непонятным причинам, тогда держите их :)
Merlyn Morgan-Graham 3.09.2010 07:02:02
Вы пробовали с отражением? Я где-то читал, что создание экземпляров с помощью рефлексии быстрее в закрытых классах
onof 3.09.2010 07:04:16
Насколько мне известно, нет. Sealed существует по другой причине - чтобы заблокировать расширяемость, которая может быть полезной / необходимой во многих случаях. Оптимизация производительности не была целью здесь.
TomTom 3.09.2010 07:15:30
... но если вы думаете о компиляторе: если ваш класс запечатан, вы знаете адрес метода, который вы вызываете для своего класса во время компиляции. Если ваш класс не запечатан, вы должны разрешить метод во время выполнения, потому что вам может потребоваться вызвать переопределение. Это, конечно, будет незначительным, но я мог видеть, что будет некоторая разница.
Dan Puzey 3.09.2010 07:33:04
Да, но, как указала ФП, такого рода действия не приносят реальных выгод. Архитектурные различия / могут быть гораздо более актуальными.
TomTom 3.09.2010 08:10:55
12 ОТВЕТОВ
РЕШЕНИЕ

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

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

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

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

58
5.08.2008 12:37:05
ссылка «бессвязная» интересна тем, что звучит как техническое совершенство, но на самом деле это чепуха. Прочитайте комментарии к статье для получения дополнительной информации. Резюме: 3 причины: версия, производительность и безопасность / предсказуемость - [см. Следующий комментарий]
Steven A. Lowe 14.10.2008 21:52:17
[продолжение] Версионирование применяется только тогда, когда нет подклассов, да, но распространите этот аргумент на каждый класс, и внезапно у вас не будет наследования и вы догадаетесь, что язык больше не объектно-ориентированный (а просто объектно-ориентированный)! [см. далее]
Steven A. Lowe 14.10.2008 21:53:16
[продолжение] пример производительности - шутка: оптимизация вызова виртуального метода; почему у запечатанного класса в первую очередь есть виртуальный метод, поскольку он не может быть разделен на подклассы? Наконец, аргумент «Безопасность / предсказуемость» явно беспочвенен: «его нельзя использовать, чтобы он был безопасным / предсказуемым». ЛОЛ!
Steven A. Lowe 14.10.2008 21:55:16
@ Стивен А. Лоу - Я думаю, что Джеффри Рихтер пытался сказать несколько окольными словами, что если вы оставите свой класс незапечатанным, вам нужно подумать о том, как производные классы могут / будут его использовать, и если у вас нет время или желание сделать это должным образом, затем запечатайте его, так как это вряд ли приведет к критическим изменениям в коде других в будущем. Это не чепуха, это здравый смысл.
Greg Beech 20.11.2009 09:58:55
Запечатанный класс может иметь виртуальный метод, поскольку он может быть производным от класса, который его объявляет. Когда вы потом, позже, объявляете переменную запечатанного класса-потомка и вызываете этот метод, компилятор может выполнить прямой вызов известной реализации, так как он знает, что нет никакого способа, которым это может отличаться от того, что в известном во время компиляции vtable для этого класса. Что касается запечатанных / незапечатанных, это другое обсуждение, и я согласен с причинами для того, чтобы сделать классы запечатанными по умолчанию.
Lasse V. Karlsen 20.11.2009 19:07:23

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

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

3
5.08.2008 12:37:07
Они должны , но, похоже, что нет. Если бы в CLR была концепция ненулевых ссылочных типов, то запечатанный класс действительно был бы лучше, так как компилятор мог бы генерировать callвместо callvirt... Я бы любил ненулевые ссылочные типы и по многим другим причинам ... вздох :-(
Orion Edwards 20.02.2012 21:04:54

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

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

SSCLI (Rotor)
SSCLI: общая языковая инфраструктура общего источника

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

2
5.08.2008 12:40:10
Тест включал создание иерархии классов с некоторыми методами, которые выполняли фиктивную работу (в основном, манипулирование строками). Некоторые из этих методов были виртуальными. Они звонили друг другу здесь и там. Затем вызывать эти методы 100, 10000 и 100000 раз ... и измерять прошедшее время. Затем запустите их после маркировки классов как запечатанных. и измерения снова. В них нет разницы.
Vaibhav 15.01.2011 00:03:57

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

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

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

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

4
14.10.2008 20:15:12
Я не запечатываю свои классы по причинам производительности. Я опечатываю их по конструктивным причинам. Проектирование для наследования является трудным, и эти усилия будут потрачены впустую большую часть времени. Я полностью согласен с предоставлением интерфейсов - это намного лучшее решение для распечатывания классов.
Jon Skeet 31.10.2008 07:09:15
Инкапсуляция, как правило, лучшее решение, чем наследование. Например, перехват исключений потока нарушает принцип подстановки Лискова, потому что вы изменили документированное поведение класса Thread, поэтому даже если вы могли бы извлечь его из него, было бы неразумно утверждать, что вы можете использовать SafeThread везде Вы можете использовать нить. В этом случае вам лучше будет инкапсулировать Thread в другой класс с другим задокументированным поведением, что вы можете сделать. Иногда вещи запечатаны для вашего же блага.
Greg Beech 20.11.2009 10:02:58
@ [Greg Beech]: мнение, а не факт - возможность наследовать от Thread, чтобы исправить отвратительный недосмотр в его дизайне, НЕ плохая вещь ;-) И я думаю, что вы преувеличиваете LSP - доказуемое свойство q (x) в этот случай - «необработанное исключение уничтожает программу», что не является «желаемым свойством» :-)
Steven A. Lowe 23.11.2009 02:51:06
Нет, но у меня была своя дрянная часть кода, где я позволил другим разработчикам злоупотреблять моими материалами, не закрывая их или не допуская случайных случаев. Большая часть моего кода в настоящее время - это утверждения и другие вещи, связанные с контрактами. И я совершенно откровенен о том, что я делаю это только для того, чтобы быть болью в ***.
Turing Complete 4.08.2010 13:54:46
Так как мы здесь делаем не по теме, точно так же, как вы ненавидите закрытые уроки, я ненавижу проглоченные исключения. Нет ничего хуже, чем когда что-то идет, но программа продолжается. JavaScript мой любимый. Вы вносите изменения в некоторый код, и внезапное нажатие кнопки абсолютно ничего не делает. Большой! ASP.NET и UpdatePanel это еще один; серьезно, если мой обработчик кнопок выбрасывает, это большое дело, и ему нужно CRASH, так что я знаю, что есть кое-что, что нужно исправить! Кнопка, которая ничего не делает, является более бесполезной, чем кнопка, которая вызывает экран сбоя!
Roman Starkov 26.08.2013 23:19:15

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

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

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

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

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

Звони и звони

139
23.05.2017 10:31:14
AFAIK, обстоятельства, в которых callиспользуется: в ситуации new T().Method(), для structметодов, для не виртуальных вызовов virtualметодов (таких как base.Virtual()), или для staticметодов. Везде еще использует callvirt.
porges 5.11.2010 02:55:52
Э-э-э ... Я понимаю, что это старое, но это не совсем правильно ... Большой выигрыш в закрытых классах - это когда JIT Optimizer может встроить вызов ... в этом случае запечатанный класс может быть огромным выигрышем ,
Brian Kennedy 1.10.2011 10:28:24
Почему этот ответ неверен. Из журнала изменений Mono: «Оптимизация девиртуализации для закрытых классов и методов, улучшающая производительность Python IronPython 2.0 на 4%. Другие программы могут ожидать аналогичного улучшения [Родриго]». Запечатанные классы могут улучшить производительность, но, как всегда, это зависит от ситуации.
Smilediver 15.11.2011 11:14:52
@Smilediver Это может улучшить производительность, но только если у вас плохой JIT (хотя и не знаю, насколько хороши .NET JIT в настоящее время - раньше это было довольно плохо). Hotspot, например, встроит виртуальные вызовы и при необходимости деоптимизирует позже - следовательно, вы платите дополнительные накладные расходы, только если вы фактически подкласса класса (и даже тогда не обязательно).
Voo 21.02.2012 01:25:07
-1 JIT не обязательно должен генерировать один и тот же машинный код для одинаковых кодов операций IL. Нулевая проверка и виртуальный вызов являются ортогональными и отдельными шагами callvirt. В случае закрытых типов JIT-компилятор все еще может оптимизировать часть callvirt. То же самое происходит, когда JIT-компилятор может гарантировать, что ссылка не будет нулевой.
rightfold 1.07.2014 10:25:13

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

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

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

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

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

4
20.11.2009 19:16:32

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

-10
26.11.2009 17:58:10
Есть несколько проблем с этим. Во-первых, вы не сбрасываете секундомер между первым и вторым тестом. Во-вторых, то, как вы вызываете метод, означает, что все коды операций будут вызываться, а не callvirt, поэтому тип не имеет значения.
Cameron MacFarland 17.12.2009 22:49:02
RuslanG, вы забыли вызвать watch.Reset () после запуска первого теста. o_O:]
user291570 15.01.2011 00:00:44
Да, код выше имеет ошибки. С другой стороны, кто может похвастаться тем, что никогда не вводит ошибок в коде? Но у этого ответа есть одна важная вещь лучше всех остальных: он пытается измерить эффект, о котором идет речь. И в этом ответе также содержится код, который все вы проверяете и [массово] понижаете (интересно, почему необходимо массовое подавление?). В отличие от других ответов. Это заслуживает уважения, на мой взгляд ... Также обратите внимание, что пользователь новичок, поэтому не очень гостеприимный подход. Некоторые простые исправления, и этот код становится полезным для всех нас. Fix + поделиться фиксированной версией, если вы решитесь
Roland Pihlakas 8.12.2016 00:07:30
Я не согласен с @RolandPihlakas. Как вы сказали, «кто может похвастаться собой, чтобы никогда не вводить ошибки в коде». Ответ -> Нет, нет. Но дело не в этом. Дело в том, что это неверная информация, которая может ввести в заблуждение другого нового программиста. Многие люди могут легко пропустить, что секундомер не был сброшен. Они могут поверить, что информация о тестах верна. Разве это не вреднее? Кто-то, кто быстро ищет ответ, может даже не прочитать эти комментарии, скорее он увидит ответ и может поверить в это.
Emran Hussain 17.05.2017 02:37:11

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

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

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

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

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

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

3
3.08.2010 14:27:15
Я полагаю, что "пропускать" (исключать) следует "излучать" (производить)?
user2864740 14.02.2017 06:35:54

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

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


подробности

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

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

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


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

23
2.11.2018 20:13:50
Процессоры Pentium класса непосредственно выбирают косвенную отправку. Иногда перенаправление указателя на функцию происходит быстрее, чем если бы (не угадывается) по этой причине.
Joshua 3.02.2011 18:52:09
Одним из преимуществ не виртуальных или закрытых функций является то, что они могут быть встроены в большем количестве ситуаций.
CodesInChaos 4.02.2011 10:04:06

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

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

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

2
1.10.2011 10:37:28
Компилятор C # будет по-прежнему использовать callvirt(вызов виртуального метода) для экземпляров методов в закрытых классах, поскольку он все еще должен выполнять проверку на нулевой объект для них. Что касается встраивания, CLR JIT может (и делает) вызовы встроенных виртуальных методов как для закрытых, так и для незапечатанных классов ... так что да. Перформанс - это миф.
Orion Edwards 20.02.2012 21:02:24

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

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

27
14.08.2017 23:11:31
Есть причина, почему вы опубликовали этот ответ дважды, а не закрыли другой как дубликат? Они выглядят как действительные дубликаты для меня, хотя это не моя область.
Flexo♦ 20.02.2012 20:52:10
Что если бы методы были длиннее (более 32 байтов кода IL), чтобы избежать встраивания и посмотреть, какая операция вызова будет использоваться? Если он встроен, вы не видите вызов, поэтому не можете судить об эффектах.
ygoe 28.06.2015 20:07:17
Я в замешательстве: почему, callvirtкогда эти методы не являются виртуальными для начала?
freakish 27.08.2018 21:27:18
@ странно, я не могу вспомнить, где я видел это, но я читал, что CLR используется callvirtдля запечатанных методов, потому что он все еще должен проверять объект на ноль перед вызовом метода, и как только вы это учтете, вы можете просто использовать callvirt. Чтобы удалить callvirtи просто перейти напрямую, им нужно будет либо изменить C #, чтобы разрешить, ((string)null).methodCall()как это делает C ++, либо им необходимо статически доказать, что объект не является нулевым (что они могли бы сделать, но не удосужились)
Orion Edwards 18.09.2018 03:10:55
Рекомендуете переходить к усилиям по копанию до уровня машинного кода, но будьте очень осторожны, делая такие заявления, как «это дает твердое доказательство того, что не может быть никакого улучшения производительности». Что вы показали, так это то, что для одного конкретного сценария нет разницы в собственном выводе. Это одна точка данных, и нельзя предполагать, что она обобщает все сценарии. Для начала, ваши классы не определяют никаких виртуальных методов, поэтому виртуальные вызовы вообще не требуются.
Matt Craig 15.12.2019 09:44:09

Чтобы действительно их увидеть, вам нужно проанализировать код JIT-Compiled (последний).

Код 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 цикла.

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

1
25.02.2020 15:00:22