.net - tricks - web framework benchmarks




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

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

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

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

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

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

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

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

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


@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 может выполнять встроенные вызовы, которые в противном случае были бы виртуальными вызовами. Итак, там, где часто называются методы, которые достаточно малы, чтобы их можно было встроить, определенно стоит подумать о закрытии класса.

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

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


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


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

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

Детали

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

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

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

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

Некоторые процессоры (включая последние чипы Intel x86) используют технику, называемую спекулятивным выполнением, чтобы использовать конвейер даже в сложившейся ситуации. Просто выберите один из путей выполнения. Но показатель попадания этой техники не так высок. А спекулятивный сбой вызывает остановку конвейера, что также приводит к огромному снижению производительности. (это полностью за счет реализации ЦП. Некоторые мобильные ЦП известны как не такие оптимизации для экономии энергии)

По сути, C # - это статически скомпилированный язык. Но не всегда. Я не знаю точного условия, и это полностью зависит от реализации компилятора. Некоторые компиляторы могут исключить возможность динамической отправки, предотвращая переопределение метода, если метод помечен как sealed . Глупые компиляторы не могут. Это преимущество производительности sealed .

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


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

Существуют случаи, когда csc может csc код операции callvirt вместо кода операции call . Однако, кажется, что такие случаи редки.

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

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

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


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

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

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

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

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

Звони и звони


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

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

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

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

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

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








performance