Анализ быстродействия типовых операций языка C#. Часть 1
Статья посвящена изучению быстродействия часто используемых функций стандартных классов языка C# в разных окружениях, таких как WPF, Windows forms, Unity и ASP.NET. Реализован асинхронный механизм инструментальной оценки быстродействия участков кода. Рассмотрены несколько версий фреймворка, включая Mono, Core и традиционный .NET Framework, чтобы выявить разницу в скорости выполнения тех или иных функциональных возможностей платформ.
Введение
Оптимизации быстродействия ПО многими разработчиками в настоящее время уделяется недостаточно внимания. Это хорошо заметно по медленной работе некоторых современных приложений и их большим размерам. В качестве примера можно привести несколько самых распространенных по назначению приложений, реализующих такие функции как набор текстов, просмотр страниц в сети Интернет, голосовая связь (рис. 1), с которым справлялись и машины конца прошлого века, обладавшие в десятки и сотни раз меньшими мощностями, чем нынешние [1].
Рисунок 1. Пример объёмов памяти, занимаемых современным ПО:
Проблема кроется, зачастую, в использовании излишне тяжелых библиотек и компонентов, которые тяжелы из-за того, что сами используют другие библиотеки и это дерево уходит своими корнями глубоко в историю. Не меньшее влияние оказывает и неправильное применение структур данных, не учитывающие сложности алгоритмов при обработке множеств элементов. Элементарные операции и типовые функции из стандартных библиотек, на первый взгляд, влияют на быстродействие в меньшей степени, однако они, ввиду частого использования тоже, порой, вносят своё воздействие в изменение быстродействия. Особенно это становится заметно при обработке больших объемов данных.
Данная статья посвящена изучению быстродействия элементарных операций и часто используемых функций языка C# в разных окружениях, таких как WPF, Windows forms, Unity. Мы рассмотрим несколько версий фреймворка и видов проекта, чтобы увидеть, есть ли разница в скорости выполнения того или иного функционала.
Для изучения напишем небольшой тестовый класс, разместим его в переносимой библиотеке и будем ее подключать в разные среды выполнения. Компактная переносимая библиотека классов, подключаемая к разным средам выполнения с исходным кодом размещена в открытом доступе на сервисе Bitbucket [2].
Методология и реализация тестового окружения
Основной функционал класса, реализованного для изменения быстродействия, следующий:
- поток, постоянно выполняющий изучаемую функцию из её делегата private Action TestAction, что позволяет частично обойти оптимизацию повторяющихся операций механизмами .Net Framework-а;
- функция замера, принимающая делегат – замеряет количество выполнений этого делегата потоком в течение 1 миллисекунды;
- функция подсчета – накапливает результаты одинаковых замеров в словаре для последующего устранения пиковых результатов, усреднения, вычисления медианного значения;
- механизм минимизации вызова сборщика мусора и замера частоты его во время тестов.
Класс тестировщика производительности представлен ниже:
public class TimeTestAsync { public int TimeMilliseconds = 1; private Action TestAction = () => { }; private Thread Thread; public int Count = 0; public readonly Dictionary<string, List<TimeResult>> Results = new Dictionary<string, List<TimeResult>>(); private void Init() { if (Thread != null) return; Thread = new Thread(TestFunk) { IsBackground = true }; Thread.Start(); } ~TimeTestAsync() => Stop(); public void Zamer(string info, Action action) { Init(); if (!Results.ContainsKey(info)) Results.Add(info, new List<TimeResult>()); var z = Zamer(action); Results[info].Add(z); } private DateTime _start; private TimeResult Zamer(Action action) { var gc = GC.CollectionCount(0); TestAction = action; Count = 0; _start = DateTime.UtcNow; Thread.Sleep(TimeMilliseconds); var end = DateTime.UtcNow.Subtract(_start); return new TimeResult() { Count = Count, Time = end, GC = GC.CollectionCount(0) — gc }; } private void TestFunk() { while (true) { TestAction.Invoke(); ++Count; } } public void Stop() { Thread?.Abort(); Thread = null; } }
Результат замера выглядит следующим образом:
public class TimeResult { public int Count; public TimeSpan Time; public int GC; public double Nanoseconds() { return Time.TotalMilliseconds / Count * 1000000; } public override string ToString() { return «\t» + Nanoseconds() + «\t» + Count; } }
По времени замера и количеству операций он определяет время выполнения тестируемой функции в наносекундах.
И, собственно, применение этого класса замеров возможно, например, в таком виде:
public class Tests { readonly TimeTestAsync Tester = new TimeTestAsync(); private const string Br = «\t»; private float ClassProperty { get; set; } static float StaticProperty { get; set; } static float StaticField = 0; float ClassField = 0; float ClassField2 = 0; string ClassStr = «»; bool ClassBool = false; private const int Min = 50, Max = 100; … public string Test() { var localRandom = new Random(); for (int i = 0; i <= 10; i++) { ClassStr = «»; ClassStringBuilder = new StringBuilder(); ClassField = localRandom.Next(Min, Max); … GC.Collect(); Tester.Zamer(«() => { ClassField++; }», () => { ClassField++; }); Tester.Zamer(«() => ClassStr = \»S1\»», () => ClassStr = «S1»); Tester.Zamer(«() => ClassStr = \»S1\» + ++ClassField», () => ClassStr = «S1» + ++ClassField); GC.Collect(); … if (i == 0) Tester.Results.Clear(); StaticField += localField; } Tester.Stop(); var tempCount = ClassField + StaticField; string s = $»Функция{Br}» + $»Среднее время на выполнение, нс{Br}» + $»Медианное время на выполнение, нс{Br}» + $»Среднеквадратичное отклонение{Br}» + $»Среднее к-во запусков за тест, раз{Br}» + $»Среднее Время теста, мс{Br}» + $»К-во тестов, раз{Br}» + $»Среднее к-во вызовов сборки мусора на тест, раз\n»; foreach (var r in Tester.Results) { var withResults = r.Value.Where(x => x.Count > 0).ToArray(); var nano = withResults.Select(x => x.Nanoseconds()).ToList(); s += $»{r.Key}{Br}» + $»{nano.Average(x => x):F2}{Br}» + $»{nano.OrderBy(x => x).ToArray()[nano.Count / 2]:F2}{Br}» + $»{StandardDeviation(nano):F2}{Br}» + $»{withResults.Average(x => (double)x.Count):F0}{Br}» + $»{withResults.Average(x => x.Time.TotalMilliseconds):F2}{Br}» + $»{withResults.Length}{Br}» + $»{withResults.Average(x => (double)x.GC):F2}\n»; } s += «\n\n» + tempCount;// + «\n\n» + Tester.GcCount return s; }
Простейший интерфейс позволяет скопировать результаты в Excel и там их обработать.
Рисунок 2. Результат измерения в простом окне WPF для последующего копирования в Excel:
Относительные результаты измерения быстродействия по рассмотренным группам операций оценивались относительно WPF по специальной формуле.
Ознакомиться с подробными результатами измерений в табличном виде вы сможете во второй части статьи.
Список литературы: 1. Моё разочарование в софте [Электронный ресурс] // Хабр. Прочитать 2. Публичный репозиторий с кодом проекта [Электронный ресурс]. Прочитать 3. Четверина О. А. Повышение производительности кода при однофазной компиляции // Программирование. 2016. № 1. С. 51–59.