Страницы

Поиск по вопросам

понедельник, 4 февраля 2019 г.

Делегат в качестве параметра

Добрый вечер уважаемые знатоки! У меня возник такой вопрос: мне нужен метод примерно следующего вида: public void MyMethod(Func del) { for(int I = 0; I < 1000; I++) del(); } Собственно в чем проблема: нужно чтобы делегат del вызывался в методе некоторое количество раз (например тысячу как указано выше), но дело в том, что Func - это некий "общий" тип - в действительности это может быть и Func и Func и Func. Более ого, T1, T2 и T3 могут быть int, float, double. Кроме того я не знаю, как можно было бы передавать кроме делегата еще и набор его параметров, с которыми он должен вызываться. Хотелось бы чтобы я имел возможность делать что-то такое: var del1 = new Func(() => 10); var del2 = new Func(i => i); var del3 = new Func((i, j) => i + j);
MyMethod(del1); MyMethod(del2, parametersForDel); MyMethod(del3, parametersForDel); Можно ли как-то написать некий общий метод MyMethod, который мог бы получать в качестве аргумента любой интересующий меня делегат указанного типа? Или же под каждый Func нужно писать свой метод? И как быть с передачей параметров для этих делегатов? Заранее спасибо


Ответ

О, тестирование производительности! Тестирование — довольно сложная тема, очень сложно сделать его полностью правильно. Считается, что это хорошее задание для новичка, но на самом деле правильные тесты производительности должен бы написать архитектор. Давайте разберём, что мы хотим тестировать. Во-первых, запуск метода один раз — не очень правильная штука. На выполнение метода влияют сотни случайных параметров: например, случайный всплеск активности антивируса или вытесненная на диск страничка кода. Поэтому необходимо прогнать метод много раз: результаты одного прогона статистически иррелевантны. (По поводу того, сколько же раз нужно прогонять тест: не берите число с потолка, а спросите коллегу-математика!) Далее, тест должен быть проведён с одними и теми же параметрами! Иначе наше усреднение не имеет смысла: мы усредняем несравнимые величины. Если в функции одна ветка кода быстрая, а другая медленная, усреднять время пробега по ним бессмысленно: надо иметь два теста, тестирующие разные ветки. Таким образом, мы должны много раз выполнить один и тот же код: вызвать функцию с одними и теми же параметрами. Значит, наш вызов функции прекрасно оборачивается в Action, и для тестирования разных функций можно использовать один и тот же метод: class PerformanceTester { const int repetitions = 1000; // может быть, нужна внешняя параметризация
static public TimeSpan ComputeAverageExecutionTime(Action a) { var executionTicks = new List(); for (int i = 0; i < repetitions; i++) executionTicks.Add(MeasureTime(a));
double averageTicks = executionTicks.Average(); return new TimeSpan((long)averageTicks); }
static private long MeasureTime(Action a) // in ticks { var stopwatch = new Stopwatch(); stopwatch.Start(); a(); stopwatch.Stop(); return stopwatch.ElapsedTicks; } } Пользоваться так: void f() { /* ... */ } void g(int arg) { /* ... */ } void h(ClassArg arg1, int arg2) { /* ... */ }
...
var r1 = PerformanceTester.ComputeAverageExecutionTime(f); var r2 = PerformanceTester.ComputeAverageExecutionTime(() => g(1)); ClassArg arg1 = new ClassArg(); // не включаем конструктор в лямбду var r3 = PerformanceTester.ComputeAverageExecutionTime(() => h(arg1, 0)); Но это — не окончательное решение. В нём есть недостатки, которые мы попробуем закрыть. Во-первых, JIT. Первый пробег функции будет медленнее из-за того, что в этот момент она будет скомпилирована just-in-time-компилятором. Плюс необходимые части кода будут загружены в память. Значит, необходимо включить «разогрев кода» в тест: static public TimeSpan ComputeAverageExecutionTime(Action a) { // разогреваем код: a();
var executionTicks = new List(); for (int i = 0; i < repetitions; i++) executionTicks.Add(MeasureTime(a));
double averageTicks = executionTicks.Average(); return new TimeSpan((long)averageTicks); } Во-вторых, среди большого числа пробегов всё равно будут случайные «выбросы» вверх и вниз. Чтобы нивелировать их, надо знать среднеквадратичное отклонение σ, и отбраковывать результаты, отличающиеся от оценочного среднего на более чем 3σ. Но для этого нужна продвинутая математика, а мы можем по-простому выкинуть наибольшее и наименьшее значения, это тоже довольно хорошая эвристика. var maxVal = executionTicks.Max(); if (executionTicks.Count(v => v == maxVal) < executionTicks.Count / 3) executionTicks.RemoveAll(v => v == maxVal);
var minVal = executionTicks.Min(); if (executionTicks.Count(v => v == minVal) < executionTicks.Count / 3) executionTicks.RemoveAll(v => v == minVal);
// здесь осталось не менее [[1000 * 2/3] * 2/3] = 444 элементов, // значит, список не пуст, и исключение не выбросится double averageTicks = executionTicks.Average(); В-третьих, давайте-ка посмотрим, что именно мы измеряем. Кроме самой функции, мы измеряем время её косвенного вызова, через лямбду и делегат. Для большинства функций это неважно: время пробега функции обычно значительно больше времени вызова делегата, так что этой тонкостью можно пренебречь. Другое дело, если ваша функция очень маленькая, для этого придётся писать код тестирования вручную. Например, такой: class PerformanceTester { static public TimeSpan ComputeExecutionTimeForSmallFunctions( Action execute1000times, Action execute1time) { // разогреваемся execute1time(); var ticks = MeasureTime(execute1000times); return new TimeSpan((long)(ticks / 1000.0)); }
// остальные функции } Используем так: void q() { /* что-то очень быстрое */ }
var r4 = PerformanceTester.ComputeExecutionTimeForSmallFunctions( () => { for (int i = 0; i < 1000; i++) q(); } q); Недостатки: в измерение всё же включается косвенный вызов делегата, но теперь делегат вызывается не 1000, а всего 1 раз, и вносит лишь 1/1000 искажения. К счастью, для маленькой функции можно смело поднимать константу 1000 до больших значений, например, до 1000000. Заодно и результаты случайных больших отклонений от среднего, с которыми мы боролись для длинных функций отбрасыванием минимального и максимального значения, нивелируются. Кроме того, ко времени выполнения функции прибавляется время на управление циклом. В-четвёртых, не забывайте, что производительность сильно зависит от настроек компилятора. Поэтому никогда не тестируйте производительность кода, скомпилированного в DEBUG-режиме. Кроме того, пробег в Visual Studio замедляет скорость кода приблизительно вдвое (!), так как отладчик специально «просит» JIT-компилятор оптимизировать поменьше. Поэтому после окончания отладки запускайте тесты производительности только вне Visual Studio, и только скомпилированные в RELEASE-режиме. В-пятых, не забывайте о наличии довольно умного оптимизатора! Он имеет право заинлайнить вызов функции, и если вы игнорируете результат, выкинуть всё вычисление! Поэтому для случая ComputeExecutionTimeForSmallFunctions возвращаемое значение надо бы как-то использовать, хотя бы запоминать в массиве, добавлять в аккумулятор или что-то ещё наподобие.

Комментариев нет:

Отправить комментарий