Анализ быстродействия типовых операций языка C#. Часть 2

Продолжаем разговор об анализе быстродействия типовых операций языка C# на платформах DOT.NET и Mono. В первой части статьи мы подробно поговорили о методологии и реализации тестового окружения. Сейчас пришло время приступать к измерениям.

Измерение проводилось на ноутбуке ASUS X556UQ: i7-7500U, 2.7 GHz, 20Г ОЗУ, Windows 10 x64.

Для оценки быстродействия реализованного тестового окружения были выбраны такие операции, как обращение к функциям, полям и свойствам класса (табл. 1 и 2). Как можно заметить, быстродействие тестовой инфраструктуры примерно сопоставимо с обычным обращением к функции или переменной. Кроме того, несмотря на предпринятые меры по усложнению оптимизации вычислений, в простых операциях заметен разгон при выполнении нескольких однотипных замеров подряд. В дальнейшем было принято решение рассматривать и сопоставлять результаты по медианному времени, поскольку оно более стабильно, чем среднее, т. к. не подвержено влиянию исключительных случаев. Хотя, как ни странно, из-за особенностей разброса результатов (и сдвига в большую сторону при чётном количестве тестов) в некоторых замерах медианный результат получен больше среднего.

Таблица 1. Результаты тестовых замеров на примере проекта WPF в Release-режиме:

Далее приведем результаты замеров в разных режимах выполнения (табл. 2), поскольку на второй фазе компиляции при создании релиз-приложения используются дополнительные механизмы оптимизации [3].

Таблица 2. Замеры в WPF в разных режимах выполнения:

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

Далее для сопоставления фреймворков будем рассматривать наиболее актуальный режим запуска – Release.

На рисунке 3 приведены результаты замеров выполнения указанных функций в релиз-проектах на WPF, Windows Forms и Unity. Как можно заметить, WPF и Windows Forms показывают примерно одинаковые результаты (в среднем по рассмотренным операциям формы медленнее на 6 %) ввиду того, что обе платформы реализованы на классическом .Net (при этом в дебаг-режиме разница между ними более существенная). В то же время, на Unity некоторые операции производятся с существенной разницей в скорости ввиду того, что основаны на MONO-Framwork-e (Unity, в среднем, медленнее на 220 %). В .Net Core-реализации заметна не менее ощутимая разница в быстродействии, как в большую, так и в меньшую сторону по разным операциям (в среднем по рассмотренным функциям на 77 % медленнее). Однако выборка функций не является достаточно презентабельной и не даёт полномочий судить о производительности того или иного фреймворка в целом.

Рисунок 3. Быстродействие обращения к методам, полям и свойствам в различных окружениях:

Здесь заметны некоторые сложности с обращением к свойствам в Unity, а также заметен более медленный расчет остатка от деления в Unity и .Net Core – примерно в 10 раз медленнее, чем в обычном .Net, при том, что остальные операции выполняются примерно с той же скоростью. В .Net Core тестовое окружение (вызов делегата) выполнялось медленнее, учитывая это, можно отметить, что остальные обращения и вычисления в нём производились с той же скоростью, что и в обычном .Net.

Быстродействие математических операций

Расссмотрим несколько наиболее популярных математических операций стандартного .Net класса Math (табл. 3 и рис. 4).

Таблица 3. Замеры быстродействия математических операций в различных окружениях:

Как можно заметить, различные математические операции выполняются в разных средах разработки с существенными отличиями в быстродействии. Например, Sin, Cos, корень, логарифм и получение модуля намного медленнее работают в Unity, чем в других версиях фреймворка, да и в среднем математика в Unity работает несколько медленнее. Хотя, например, арктангенс вычисляется быстрее.

Интересен тот факт, что возведение в степень с помощью функции Pow работает с одной скоростью для целых и дробных степеней и на пару порядков медленнее умножения (за исключением .Net Core, где оно приближается по скорости к простым арифметическим операциям).

Рисунок 4. Замеры быстродействия математических операций в различных окружениях:

Быстродействие функций работы с коллекциями

Приведем результаты исследования работы с коллекциями коротко на примере массивов из 1 и 1000 элементов и таких часто используемых функций, как Contains – поиск элемента, FirstOrDefault – аналогичная функция в LINQ-расширениях, Count – подсчёт с помощью LINQ. ExistElement – случай для элемента, который присутствует в массиве, а NotExistElement – соответственно, для несуществующего элемента.

Таблица 4. Замеры быстродействия работы с массивами в различных окружениях:

Как можно заметить, различия в скорости выполнения присутствуют и увеличиваются с увеличением объёма массива. Практически во всех рассмотренных случаях Mono реализация Unity несколько уступает по скорости.

Что интересно, проверка наличия элемента с помощью встроенной функции Contains проходит быстрее, чем с помощью LINQ-расширения примерно в 3–5 раз в классическом и Core-фреймворках и в 3 раза медленнее Юнити на 1000 элементах. А на 1 элементе LINQ в 1,5 раза быстрее в классическом фреймворке и быстрее в Юнити.

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

Быстродействие работы со строками

На рисунке 5 приведены замеры быстродействия строковых операций. Здесь Str1 = «1», Str10 = «1234567890», Str100 = «1234567890123…90» – до длины в 100 символов.

Можно отметить, что операции сложения строк в целом довольно тяжеловесны, также, как и преобразования чисел в строки. Быстродействие методов String.Format уступает по скорости интерполяции строк примерно на 15 %, а интерполяция, в свою очередь, уступает конкатенации примерно на столько же. String.Join, естественно существенно опережает обе из перечисленных функций.

Рисунок 5. Быстродействие работы со строками в различных окружениях:

Выводы

При сравнении результатов быстродействия WPF и WindowsForms в релиз-режиме получено, что средняя разница быстродействия операций по разным группам составляет до 10 %, что может быть обусловлено погрешностями измерений. В целом же все операции выполняются примерно с одинаковой скоростью, что неудивительно, ввиду единого фреймворка. Это довольно очевидно и без исследования и приведено в большей степени для того, чтобы можно было со стороны оценить погрешность измерения.

Что касается сравнения с Юнити и .Net Core, то фреймворк уже отличается, ввиду чего и отличия более существенные.

Вызовы пустых функций и обращения к переменным в Юнити выполняются, в среднем, на 43 % медленнее (за счет обращения к свойствам), математические вычисления – на 25 % (за счет таких функций, как sin, cos, tan, sqrt, abs – медленнее в 10-20 раз, тогда как atan, random, pow выполнялись быстрее в 1,5–2 раза), работа с коллекциями – на 25 %, а работа со строками на 10 % медленнее. Первоначальные замеры показывали, что работа со строками в Юнити происходит медленнее, чем в WPF, на 98 % по формуле 1 (в 100 раз), но после минимизации вызовов сборщика мусора этот результат существенно улучшился. Тем не менее при относительно долгом функционировании, сборка мусора в любом случае внесёт свой вклад в быстродействие реальной программы.

В .Net Core базовые операции в среднем на 43 % медленнее, математические имеют довольно большой разброс и выполняются на 17 % быстрее (до 20 раз быстрее для atan, pow и до 4 раз медленнее для log), коллекции работают немного медленнее с малыми объемами и немного быстрее с большими – в среднем одинаково. Что же касается строк, то здесь среднее быстродействие то же, что и в WPF. Однако быстродействие выполнения отдельного функционала различается до 2–3 раз (наибольшие различия: IsNullOrWhiteSpace в WPF быстрее в 6 раз, а Contains в 3 раза медленнее).

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

Список литературы: 1. Моё разочарование в софте [Электронный ресурс] // Хабр. Прочитать 2. Публичный репозиторий с кодом проекта [Электронный ресурс]. Прочитать 3. Четверина О. А. Повышение производительности кода при однофазной компиляции // Программирование. 2016. № 1. С. 51–59.