Исследование скорости вызова метода различными способами

от автора


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

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: 660 ms 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 object Get(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 Dictionary<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?). Способов сделать это масса, из которых можно выделить несколько основных. они и перечислены в таблице в начале статьи.

Про код

Ниже используется вызов кода приведенного в спойлере. Из кода убраны замеры времение, замеры делались через Stopwatch. Для анализа интересовало относительное время выполнения, а не абсолютное, поэтому железо и другие параметры не важны. Тестировал на разных пк, результаты схожие.
Также стоит заметить, что при вызовах не учитывается время на предобработку, в которой добираемся до нужного метода, т.к. в реальных условиях, в высоконагруженных задачах такие действия выполняются только раз, и результат кэшируется, соответственно это время не имеет значения при анализе.

Прямой вызов


Просто дергаем метод напрямую. В таблице, результаты прямого вызова в первой строке, значение 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 вызов делегата. По сути выкинули проверки на совпадение типов. И ускорились еще в два раза по сравнению с оптимизированным вызовом делегатов.

Код проекта

Вернуться к результатам

ссылка на оригинал статьи https://habrahabr.ru/post/279367/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *