Анализ быстродействия типовых операций языка 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.