Дослідження швидкості виклику методу різними способами

Результат і висновки для тих хто не любить довгий текст


100.000 викликів, 20 ітерацій тіста, x86 100.000 викликів, 20 ітерацій тіста, x64 1.000.000 викликів, 10 ітерацій тіста, x86 1.000.000 викликів, 10 ітерацій тіста, x64
Прямий виклик Min: 10 ms
Max: 11 ms
Mean: 10,15 ms
Median: 10 ms
Abs: 1

min: 5 ms
Max: 5 ms
Mean: 5 ms
Median: 5 ms
Abs: 1

Min: 107 ms
Max: 110 ms
Mean: 107,8 ms
Median: 107,5 ms
Abs: 1

Min: 55 ms
Max: 57 ms
Mean: 55,3 ms
Median: 55 ms
Abs: 1

Виклик через відображення Min: 336 ms
Max: 455 ms
Mean: 359,45 ms
Median: 342,5 ms
Rel: 34

Min: 327 ms
Max: 358 ms
Mean: 336,1 ms
Median: 335 ms
Rel: 67

Min: 3362 ms
Max: 3419 ms
Mean: 3387,2 ms
Median: 3385 ms
Rel: 31

Min: 3334 ms
Max: 3427 ms
Mean: 3370,2 ms
Median: 3363 ms
Rel: 61

Виклик через делегат Min: 657 ms
Max: 1376 ms
Mean: 728,4 ms
Median: 684,5 ms
Rel: 68

Min: ms 660
Max: 726 ms
Mean: 686,05 ms
Median: 685,5 ms
Rel: 137

Min: 6586 ms
Max: 13735 ms
Mean: 7340,6 ms
Median: 6632,5 ms
Rel: 62

Min: 6501 ms
Max: 6919 ms
Mean: 6798 ms
Median: 6828 ms
Rel: 124

Виклик через делегат з оптимизациями Min: 67 ms
Max: 86 ms
Mean: 69,2 ms
Median: 67,5 ms
Rel: 6.7

Min: 79 ms
Max: 88 ms
Mean: 80,95 ms
Median: 80 ms
Rel: 16

Min: 683 ms
Max: 7373 ms
Mean: 1360,7 ms
Median: 691 ms
Rel: 6.5

Min: 775 ms
Max: 814 ms
Mean: 789,8 ms
Median: 783,5 ms
Rel: 12

Виклик через dynamic Min: 47 ms
Max: 50 ms
Mean: 48 ms
Median: 48 ms
Rel: 5

Min: 39 ms
Max: 43 ms
Mean: 41,25 ms
Median: 41 ms
Rel: 8

Min: 479 ms
Max: 518 ms
Mean: 492 ms
Median: 487 ms
Rel: 4.5

Min: 379 ms
Max: 420 ms
Mean: 392,1 ms
Median: 387,5 ms
Rel: 7



При використанні .NET Framework 3.5 краще всього використовувати виклик методів через делегат з оптимізацією виклику. Для .NET Framework 4.0+ відмінним вибором буде використання dynamic.


Трохи оффтопа, про причини дослідженняНапишемо наступний код:
class SampleGeneric<T>
{
public long Process(T obj)
{
return String.Format("{0} [{1}]", obj.ToString(), obj.GetType().FullName).Length;
}
}

class Container
{
private static Dictionary<Type, object> _instances = new Dictionary<Type, object>();

public static void Register<T>(SampleGeneric<T> instance)
{
if (false == _instances.ContainsKey(typeof(T)))
{
_instances.Add(typeof(T), instance);
}
else
{
_instances[typeof(T)] = instance;
}
}

public static SampleGeneric<T> Get<T>()
{
if (false == _instances.ContainsKey(typeof(T))) throw new KeyNotFoundException();
return (SampleGeneric<T>)_instances[typeof(T)];
}

public static Get object(Type type)
{
if (false == _instances.ContainsKey(type)) throw new KeyNotFoundException();
return _instances[type];
}
}


Подібний код використовується досить часто, і в ньому є одна незручність, — в C# не можна зберігати колекцію generic типів явним чином. Всі поради, які я знаходив зводяться до виділення базового non-generic класу, інтерфейсу або абстрактного класу, який і буде вказаний для зберігання. Тобто отримаємо щось на зразок такого:
public interface ISampleGeneric { }
class SampleGeneric<T> : ISampleGeneric
//
private static Dictionary<Type, ISampleGeneric> _instances = new Dictionary<Type, ISampleGeneric>();


На мій погляд, було б зручно додати в мову можливість писати таким чином:
// Помилка Type expected
Словник<Type, SampleGeneric<>>

Особливо враховуючи, що роблячи generic тип через рефлексію, ми користуємося подібною конструкцією:
typeof(SampleGeneric<>).MakeGenericType(typeof(string))

Але повернемося до проблеми. Тепер уявімо що нам потрібно отримати конкретний інстанси, але отримати його нам потрібно non-generic методі. Наприклад, метод який приймає об'єкт і виходячи з його типу повинен підібрати обробник.
void NonGenericMethod(object obj)
{
var handler = Container.Get(obj.GetType());
}


Обробник ми отримаємо, але середовище не дозволить написати тепер handler.Process(obj), а якщо і напишемо, компілятор ругнется на відсутність такого методу.
Ось тут теж могла б бути від розробників C# конструкція на зразок:
Container.GetInstance<fromtype(obj.GetType())>().Process(obj);

, але її немає, а метод викликати потрібно (хоча враховуючи Roslyn може вже є подібне в нових IDE?). Способів зробити це маса, з яких можна виділити декілька основних. вони перераховані в таблиці на початку статті.


Про-код

Нижче використовується виклик коду наведеного в спойлері. З коду прибрані виміри времение, виміри робилися через Секундомір. Для аналізу цікавило відносний час виконання, а не абсолютна, тому залізо та інші параметри не важливі. Тестував на різних пк, результати схожі.
Також варто зауважити, що при виконанні викликів не враховується час на предобработку, в якій добираємося до потрібного методу, оскільки в реальних умовах, в високонавантажених завданнях такі дії виконуються тільки раз, і результат кешується, відповідно цей час не має значення при аналізі.

Прямий виклик

Просто смикаємо метод безпосередньо. У таблиці, результати прямого виклику в першому рядку, значення Abs відповідно завжди одиниця, щодо нього в інших рядках можна бачити уповільнення викликів іншими способами виклику методу (в значенні Rel).
public static TestResult TestDirectCall(DateTime arg)
{
var instance = Container.Get<DateTime>();
long summ = 0;
for (long i = 0; i < ITERATION_COUNT; i++)
{
summ += instance.Process(arg);
}
// return
}


Виклик через Reflection

Самий простий і доступний спосіб, який хочеться використати в першу чергу. Забрали метод з таблиці методів і смикаємо його через Invoke. У той же час, один з найбільш повільних способів.
public static TestResult TestReflectionCall(object arg)
{
var instance = Container.Get(arg.GetType());
var method = instance.GetType().GetMethod("Process");
long summ = 0;
for (long i = 0; i < ITERATION_COUNT; i++)
{
summ += (long)method.Invoke(instance, new object[] { arg });
}
// return
}


Виклик через делегат і через делегат з додатковою оптимізацією

Код для створення делегата
private static Delegate CreateDelegate(object target, MethodInfo method)
{
var methodParameters = method.GetParameters();
var arguments = methodParameters.Select(d => Expression.Parameter(d.ParameterType, d.Name)).ToArray();
var instance = target == null ? null : Expression.Constant(target);
var methodCall = Expression.Call(instance, method, arguments);
return Expression.Lambda(methodCall, arguments).Compile();
}


Відповідно код тесту стає наступним:
public static TestResult TestDelegateCall(object arg)
{
var instance = Container.Get(arg.GetType());
var hook = CreateDelegate(instance, instance.GetType().GetMethod("Process"));
long summ = 0;
for (long i = 0; i < ITERATION_COUNT; i++)
{
summ += (long)hook.DynamicInvoke(arg);
}
// return
}

Отримали уповільнення порівняно з Reflection способом ще в два рази, можна було б викинути цей метод, але є відмінний спосіб прискорити процес. Чесно скажу що підгледів його у проекті Impromptu, а саме в цьому месте.

Код оптимізації виклику делегата
internal static object FastDynamicInvokeDelegate(Delegate del, params dynamic[] args)
{
dynamic tDel = del;
switch (args.Length)
{
default:
try
{
return del.DynamicInvoke(args);
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
}
#region Optimization
case 1:
return tDel(args[0]);
case 2:
return tDel(args[0], args[1]);
case 3:
return tDel(args[0], args[1], args[2]);
case 4:
return tDel(args[0], args[1], args[2], args[3]);
case 5:
return tDel(args[0], args[1], args[2], args[3], args[4]);
case 6:
return tDel(args[0], args[1], args[2], args[3], args[4], args[5]);
case 7:
return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
case 8:
return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
case 9:
return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
case 10:
return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]);
case 11:
return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10]);
case 12:
return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11]);
case 13:
return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12]);
case 14:
return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13]);
case 15:
return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14]);
case 16:
return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15]);
#endregion
}
}



Трохи змінюємо код тесту

public static TestResult TestDelegateOptimizeCall(object arg)
{
var instance = Container.Get(arg.GetType());
var hook = CreateDelegate(instance, instance.GetType().GetMethod("Process"));
long summ = 0;
for (long i = 0; i < ITERATION_COUNT; i++)
{
summ += (long)FastDynamicInvokeDelegate(hook, arg);
}
// return
}


І отримуємо десятикратне прискорення порівняно з звичайним викликом делегата. На поточний момент це кращий варіант з розглянутих.

Виклик через dynamic

І переходимо до головного героя (якщо звичайно ви не підтримуєте legacy проекти створені до .NET 4.0)
public static TestResult TestDynamicCall(dynamic arg)
{
var instance = Container.Get(arg.GetType());
dynamic hook = CreateDelegate(instance, instance.GetType().GetMethod("Process"));
long summ = 0;
for (long i = 0; i < ITERATION_COUNT; i++)
{
summ += hook(arg);
}
// return
}


Все, що ми зробили в порівнянні з викликом через делегат, додали ключове слово dynamic, ніж дозволили середовищі виконання під час роботи самої побудувати через DLR виклик представника. По суті викинули перевірки на збіг типів. І прискорилися ще в два рази в порівнянні з оптимізованим викликом делегатів.

Код проекту

Повернутися до результатів

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.